Skip to content

feat(sentinel-graph): Add Microsoft Sentinel Graph API integration (public preview)#1088

Open
SinsBre wants to merge 10 commits into
masterfrom
feature/sentinel-graph
Open

feat(sentinel-graph): Add Microsoft Sentinel Graph API integration (public preview)#1088
SinsBre wants to merge 10 commits into
masterfrom
feature/sentinel-graph

Conversation

@SinsBre
Copy link
Copy Markdown
Contributor

@SinsBre SinsBre commented Apr 7, 2026

Summary

Adds a SentinelGraphMixin plugin that integrates with the Microsoft Sentinel Graph REST API (now in public preview). Users can query their Sentinel custom graph instances using GQL and visualize the results directly in Graphistry.

import graphistry

# Discover available graph instances
graphistry.configure_sentinel_graph(graph_instance="placeholder")
instances = graphistry.sentinel_graph_list()

# Connect to a specific instance and query
graphistry.configure_sentinel_graph(graph_instance=instances.iloc[0]['name'])
viz = graphistry.sentinel_graph("MATCH (n)-[e]->(m) RETURN * LIMIT 100")
viz.plot()

# Service principal auth (for production)
graphistry.configure_sentinel_graph(
    graph_instance="MyInstance",
    tenant_id=os.environ['AZURE_TENANT_ID'],
    client_id=os.environ['AZURE_CLIENT_ID'],
    client_secret=os.environ['AZURE_CLIENT_SECRET']
)

# Clean up cached token
graphistry.sentinel_graph_close()

Features

  • GQL query execution against any Sentinel custom graph instance
  • sentinel_graph_list() — discover available instances via GET /graphs/graph-instances?graphTypes=Custom
  • responseFormats parameter — defaults to ["Graph"]; pass ["Table", "Graph"] to request both formats in a single call
  • Multiple auth flows — interactive browser, service principal, device code, custom TokenCredential, DefaultAzureCredential fallback
  • Token caching with 5-minute expiry buffer and automatic refresh
  • Retry with exponential backoff on network failures
  • Security defaults — HTTPS enforced, SSL verification on, tokens never logged

What this PR adds (new files)

File Purpose Lines
graphistry/plugins/sentinel_graph.py SentinelGraphMixin — config, auth, query, list, parse +608
graphistry/plugins_types/sentinel_graph_types.py SentinelGraphConfig dataclass + error types +47
graphistry/tests/plugins/test_sentinel_graph.py 44 unit tests + 1 skipped integration test +706
demos/demos_databases_apis/microsoft/sentinel/sentinel_graph_examples.ipynb Worked-example notebook +333
setup.py New [sentinel-graph] extra (azure-identity) +1

Plus minimal wiring in graphistry/__init__.py, graphistry/pygraphistry.py, and graphistry/client_session.py to expose configure_sentinel_graph / sentinel_graph / sentinel_graph_list / sentinel_graph_close / sentinel_graph_from_credential at the top-level API and per-Plotter.

Notes for reviewers

  • Targets the public preview API format (result.graph.{nodes,edges} envelope). The pre-preview sys_* field format has been removed.
  • The Graph response format is preferred over Table for Graphistry's use case — it gives the full connected subgraph rather than just the per-row RETURN clause matches.
  • Integration tests exist but are skipped by default (@pytest.mark.integration) — they require live credentials.
  • Optional dependency: azure-identity. Install via pip install graphistry[sentinel-graph].

Test plan

  • python -m pytest graphistry/tests/plugins/test_sentinel_graph.py -v — 44 unit tests pass, 1 integration test skipped
  • Manual smoke test against a live Sentinel custom graph instance
  • Verify sentinel_graph_list() returns correct instance metadata
  • Verify interactive browser auth and service principal auth flows

CI status (as of latest push)

  • ReadTheDocs: passing
  • CodeQL: passing
  • python-lint-types matrix (3.8 / 3.9 / 3.10 / 3.11 / 3.12 / 3.13 / 3.14): all passing
  • cypher-frontend gates (surface-guard, differential-parity, strict-typing): passing
  • Larger test matrix (test-gfql-core, test-minimal-python, test-docs): running for the first time after the master merge

SinsBre and others added 6 commits October 7, 2025 00:22
Implements a new plugin for querying Microsoft Sentinel Graph API (Microsoft
Security Platform) and visualizing graph data with Graphistry.

Key Features:
- Simple API following Kusto plugin pattern: configure_sentinel_graph() + sentinel_graph(query)
- Auto-converts API responses to Graphistry nodes/edges via defensive JSON parsing
- Supports multiple authentication methods:
  - Service principal (tenant_id, client_id, client_secret)
  - Interactive browser credential (default)
  - Device code authentication
  - Custom TokenCredential
- Production-ready security hardening:
  - HTTPS enforcement with HTTP endpoint rejection
  - SSL certificate verification (enabled by default)
  - Sanitized error messages to prevent information disclosure
  - Credentials and tokens never logged
  - Query content not logged (could contain sensitive filters)
  - Token storage with repr=False to prevent accidental exposure
- Robust error handling:
  - HTTP retry with exponential backoff
  - Configurable timeout and max retries
  - Token caching with 5-minute expiry buffer
- Comprehensive test coverage (30+ unit tests)

Files Added:
- graphistry/plugins/sentinel_graph.py - Main plugin implementation
- graphistry/plugins_types/sentinel_graph_types.py - Type definitions and config
- graphistry/tests/plugins/test_sentinel_graph.py - Complete test suite
- demos/demos_databases_apis/microsoft/sentinel/sentinel_graph_examples.ipynb - Demo notebook

Files Modified:
- graphistry/client_session.py - Add sentinel_graph config property
- graphistry/plotter.py - Integrate SentinelGraphMixin
- setup.py - Add 'sentinel-graph' extras dependency

Example Usage:
  import graphistry
  from azure.identity import InteractiveBrowserCredential

  g = graphistry.configure_sentinel_graph(
      graph_instance='YourGraphInstance',
      credential=InteractiveBrowserCredential()
  )

  viz = g.sentinel_graph('MATCH (n)-[e]->(m) RETURN * LIMIT 50')
  viz.plot()

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Create comprehensive test fixture module to enable testing Sentinel Graph
functionality without requiring live Azure credentials or actual threat
intelligence data. This improves developer experience and enables faster
test iteration.

**What changed:**

- Created `graphistry/tests/fixtures/` package with synthetic response data
- Added `sentinel_graph_responses.py` with 9 fixture functions covering:
  - Minimal/simple graphs for basic testing
  - Duplicate node scenarios for deduplication logic
  - Malformed JSON for error handling validation
  - Empty responses for edge case coverage
  - Complex multi-type graphs for real-world simulation
  - Orphan edges, special characters, and null properties
- Updated `test_sentinel_graph.py` to use fixtures instead of hardcoded constants
- Reformatted notebook cells (Jupyter format standardization)

**Benefits:**

- Tests can run without Azure credentials or Sentinel Graph instance
- Fixtures mimic actual API response structure (Graph.Nodes + RawData.Rows)
- Easier to add new test cases by creating additional fixtures
- Validates parsing logic across diverse response scenarios
- All fixtures are JSON-serializable and structure-validated

**Testing:**

All 9 fixtures validated successfully with proper response structure.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The Microsoft Sentinel Graph API returns fields with sys_* prefix
(sys_sourceId, sys_targetId, sys_label) instead of the underscore
prefix (_sourceId, _targetId, _label) that was originally expected.

- Update node/edge extraction to detect both _* and sys_* field formats
- Dynamically capture all properties from nodes and edges instead of
  hardcoding specific fields
- Normalize key fields (id, label, source, target, edge) while
  preserving all original properties
- Add test fixture mimicking actual Sentinel Graph API response format
- Add tests for sys_* field format parsing
Enable cleaner API usage without requiring bind():
  graphistry.configure_sentinel_graph('instance')
  graphistry.sentinel_graph(query)

Changes:
- Add GraphistryClient wrapper methods for sentinel_graph functions
- Export sentinel_graph methods at module level in pygraphistry.py
- Re-export in __init__.py for public API access
- Update docstring examples to use module-level pattern

Security: No additional risk - module-level access uses same session
model as bind() pattern. Tokens and credentials remain protected.
Microsoft moved Sentinel custom graph to public preview with a new
response schema. Updates the plugin to match:

- Rewrite response parsing for new envelope: result.graph.{nodes,edges}
  and result.rawData.tables (replacing the old Graph/RawData format)
- Add responseFormats request parameter (default: ["Graph"])
- Add sentinel_graph_list() to discover available graph instances via
  GET /graphs/graph-instances?graphTypes=Custom
- Remove sys_* / JSON-encoded-string field handling (pre-preview only)
- Rewrite test fixtures and tests for new schema; add TestSentinelGraphList,
  TestResponseFormats, and TestTableFormatParsing test classes
- Update demo notebook with list-then-configure pattern and responseFormats example

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@SinsBre SinsBre requested review from DataBoyTX and exrhizo April 7, 2026 18:02
Comment thread graphistry/tests/plugins/test_sentinel_graph.py Fixed
SinsBre added 4 commits May 6, 2026 13:50
- Replace substring containment with startswith host-prefix check
  to satisfy CodeQL 'incomplete URL substring sanitization' (alert #9).
  An attacker-controlled URL like https://evil.com/api.securityplatform.microsoft.com/
  would still pass the old 'in url' check; the startswith form anchors
  the host to position 0.
- Remove unused 'result =' assignment in test_execute_query_retry_on_timeout
  (F841 — surfaced by python-lint-types CI; assertions only check
  retry call counts, not the return value).

Both fixes are test-only; runtime sentinel_graph.py is unchanged.
- Add empty 'outputs' and 'execution_count' to two code cells in
  sentinel_graph_examples.ipynb (cells 7 and 9). nbformat 4 requires
  both keys on every code cell; nbsphinx errored with
  AttributeError: outputs during RTD's sphinx-build, killing the docs
  build after the pytz import was resolved upstream.
- Add azure.core.* and azure.identity to mypy.ini ignore_missing_imports.
  Mirrors the existing azure.kusto.* entry. Without these, mypy 1.20
  flags the 'azure.core.credentials' and 'azure.identity' imports in
  sentinel_graph.py / sentinel_graph_types.py as missing stubs, failing
  python-lint-types CI on every Python version.

Verified locally: ruff/mypy/pytest all green; nbformat.validate passes;
'import graphistry' loads cleanly.
Return the locally-bound token (typed 'str') instead of cfg._token
(typed 'Optional[str]'). Functionally equivalent — token is assigned
to cfg._token on the previous line — but mypy 1.14 (the pinned mypy
on the Python 3.8 lockfile) does not narrow the field-access form and
flagged: 'Incompatible return value type (got str | None, expected str)'.

mypy 1.20+ (3.10+ lockfiles) accepted the original code, which is why
the failure was 3.8-specific. Verified clean with both mypy 1.14 and
1.20.2; sentinel-graph tests still pass.
@SinsBre SinsBre marked this pull request as ready for review May 14, 2026 17:41
@SinsBre SinsBre requested review from aucahuasi, lmeyerov and tanmoyio and removed request for exrhizo May 14, 2026 17:42
@lmeyerov
Copy link
Copy Markdown
Contributor

@SinsBre ci is red

@lmeyerov
Copy link
Copy Markdown
Contributor

Autoreview, generally should work until .agents/skills/review/SKILL.md converges:


• Review skill completed to convergence for PR #1088. I kept it read-only and internal: no PR comments, no source edits.

  Report artifacts:

  - plans/review-pr-1088/final-report.md
  - plans/review-pr-1088/plan.md

  Result: not merge-ready.

  Findings:

  - BLOCKER: CI is red. test-minimal-python fails on 3.8 and 3.14 because Sentinel auth tests patch azure.identity.*, but azure-identity is only in the optional sentinel-graph extra.
  - IMPORTANT: graphistry.sentinel_graph_list() is advertised/defined in pygraphistry.py but not exported from graphistry/__init__.py.
  - IMPORTANT: parsed Sentinel properties can overwrite binding columns like id, source, and target, silently corrupting the graph.
  - IMPORTANT: DefaultAzureCredential fallback only handles InteractiveBrowserCredential() construction failure, not get_token() failure.
  - IMPORTANT: no CHANGELOG.md update despite the new public connector/API.

  Validation:

  - Review converged after 3 waves: wave 1 found issues; waves 2 and 3 had no new findings.
  - ruff and compileall passed for touched Sentinel modules/tests.
  - Focused Sentinel pytest failed locally with the same Azure dependency issue seen in CI.
  - Latest PR checks still show test-minimal-python failing.

@lmeyerov
Copy link
Copy Markdown
Contributor

expanded autoreview


PR #1088 Internal Review Report

Target: #1088
Mode: findings
Fixes: deferred/read-only
Review skill status: converged after 3 code/API waves, a docs-quality addendum, a DRY/architecture addendum, and an explicit priority-calibration pass.

Summary

PR #1088 adds a Microsoft Sentinel Graph connector, tests, fixtures, optional sentinel-graph extra, notebook, and public API wiring. The implementation is not merge-ready as reviewed: CI is red, and the calibrated review found one merge blocker, seven important issues, and three suggestions.

Wave Table

Wave New BLOCKER New IMPORTANT New SUGGESTION Advance
1 1 4 0 Substantive findings
2 0 0 0 No new findings; confirmed F1-F5
3 0 0 0 No new findings; convergence reached
4 docs 0 3 1 Substantive notebook/docs findings
5 docs 0 0 0 No new docs findings
6 docs 0 0 0 No new docs findings; docs addendum converged
7 arch 0 1 1 Substantive DRY/architecture findings
8 arch 0 0 0 No new architecture findings
9 arch 0 0 0 No new architecture findings; architecture addendum converged
10 priority 0 -1 +1 Downgraded F10 from IMPORTANT to SUGGESTION
11 priority 0 0 0 No additional severity changes; calibration converged

Blockers

F1 — CI fails because Sentinel auth tests require optional Azure dependency

Severity: BLOCKER
Status: UNFIXED
Files: graphistry/tests/plugins/test_sentinel_graph.py, setup.py, .github/workflows/ci.yml, bin/test-minimal-lite.sh

Evidence:

  • Tests patch azure.identity.* at graphistry/tests/plugins/test_sentinel_graph.py:133, :155, :192, :211.
  • azure-identity is optional under the sentinel-graph extra at setup.py:60.
  • The required minimal CI job installs the package with --no-deps and runs minimal tests at .github/workflows/ci.yml:635-652.
  • bin/test-minimal-lite.sh:19-39 excludes existing optional plugin tests like Kusto/Spanner but not the new Sentinel test file.
  • Local focused run: python3 -m pytest -q graphistry/tests/plugins/test_sentinel_graph.py -> 4 failed, 40 passed, 1 skipped.
  • GitHub Actions log for run 25434926860, job 74610502161: same four failures with ModuleNotFoundError: No module named 'azure'.
  • Latest gh pr checks 1088 --repo graphistry/pygraphistry: test-minimal-python (3.8) and (3.14) still fail.

Expected fix direction: make auth tests independent of installed Azure packages via module injection/mocking, skip those tests when azure.identity is absent, install the extra in the intended CI profile, or move/exclude this optional plugin suite consistently with existing optional connectors.

DRY/architecture note: Kusto already uses an optional-dependency guard pattern in graphistry/tests/test_kusto.py:7-12, :119-123, and :236-238; Sentinel auth tests should reuse that test convention or an equivalent module-injection approach.

Important

F2 — Top-level graphistry.sentinel_graph_list() is not exported

Status: UNFIXED
Evidence:

  • GraphistryClient.sentinel_graph_list() exists at graphistry/pygraphistry.py:2257-2259.
  • graphistry/__init__.py:49-52 re-exports the other Sentinel helpers but omits sentinel_graph_list.
  • Import smoke: hasattr(graphistry, "sentinel_graph_list") is False.

Impact: The PR advertises list/discovery as a top-level API, but import graphistry; graphistry.sentinel_graph_list() fails.

F3 — Response properties can overwrite binding columns

Status: UNFIXED
Evidence:

  • Node parsing sets id/label/labels, then overlays properties at graphistry/plugins/sentinel_graph.py:437-440.
  • Edge parsing sets source/target/id/edge/labels, then overlays properties at graphistry/plugins/sentinel_graph.py:498-505.
  • Table fallback paths repeat the pattern at :463 and :531.
  • Crafted response reproduced overwritten id, source, and target binding values.

Impact: Sentinel properties named id, label, source, or target can silently corrupt Graphistry node/edge bindings and produce a wrong graph.

Architecture note: Kusto avoids this class of collision by asking Kusto to emit reserved binding columns g_NodeId, g_src, and g_dst before binding at graphistry/plugins/kusto.py:343-352.

F4 — DefaultAzureCredential fallback does not run on browser token failure

Status: UNFIXED
Evidence:

  • Fallback wraps only InteractiveBrowserCredential() construction at graphistry/plugins/sentinel_graph.py:368-377.
  • Actual token acquisition happens later at :380; failure there is caught by the outer except and converted to SentinelGraphConnectionError at :388-393.
  • Smoke test with InteractiveBrowserCredential.get_token() raising showed default_called False.

Impact: The PR promises DefaultAzureCredential fallback, but the common headless/server failure mode does not fall back.

F5 — Missing changelog entry

Status: UNFIXED
Evidence:

  • CONTRIBUTING.md:55-59 requires manual CHANGELOG.md updates for PRs.
  • CHANGELOG.md is absent from git diff --name-only origin/master...HEAD.

Impact: New connector/API/extra/notebook behavior is absent from release notes.

F6 — Notebook discovery/auth cells are not runnable in order

Status: UNFIXED
Evidence:

  • The notebook says to use sentinel_graph_list() for discovery at demos/demos_databases_apis/microsoft/sentinel/sentinel_graph_examples.ipynb:62.
  • The next cell uses graph_instance_name and credential before they are defined at :66.
  • The following cell only then creates credential = InteractiveBrowserCredential() and reconfigures with hard-coded 'YourGraphInstance' at :77-86.

Impact: Running the notebook top-to-bottom fails before the first query and does not demonstrate the advertised discovery flow.

F7 — Notebook examples use _node / _edge instead of _nodes / _edges

Status: UNFIXED
Evidence:

  • Correct plural usage appears at demos/demos_databases_apis/microsoft/sentinel/sentinel_graph_examples.ipynb:92 and :118.
  • Later cells use singular _node / _edge at :110, :132-143, and :195.
  • Local Plotter check confirmed _nodes / _edges are DataFrames, while _node is a binding column name and _edge can be None.

Impact: These cells either report misleading counts or fail at runtime when users inspect results.

F8 — Notebook claims raw Table data is available through the demonstrated API

Status: UNFIXED
Evidence:

  • The notebook says response_formats=["Table", "Graph"] gets raw tabular data at demos/demos_databases_apis/microsoft/sentinel/sentinel_graph_examples.ipynb:66 and says the Table section is available for inspection at :244.
  • The demonstrated API is both_formats_viz = g.sentinel_graph(...) at :237.
  • sentinel_graph() returns a parsed Plottable; raw rawData.tables is used only as a fallback source for node/edge extraction and is not exposed on the returned object.

Impact: The docs promise inspectable raw tables that users cannot access through the example.

Suggestions

F9 — Notebook section order and discoverability cleanup

Status: DEFERRED
Evidence:

  • "Example 6: Error Handling" at demos/demos_databases_apis/microsoft/sentinel/sentinel_graph_examples.ipynb:230-232 is followed by Table/Graph response-format code at :237.
  • The "Requesting Both Graph and Table Formats" heading appears after that code at :243-244.
  • The actual error-handling cell appears later at :253-259.
  • docs/source/notebooks/plugins.connectors.rst links Kusto and Spanner examples but not Sentinel.

Impact: The notebook reads out of order and is harder to discover from docs navigation.

F11 — Share a reserved-column policy for provider graph normalization

Status: DEFERRED
Evidence:

  • Kusto converts graph output to reserved binding columns g_NodeId, g_src, and g_dst at graphistry/plugins/kusto.py:343-352.
  • Spanner converts provider graph records to identifier, source, and destination, then spreads provider properties at graphistry/plugins/spanner.py:243-292.
  • Sentinel converts provider graph records to id, source, and target, then spreads provider properties at graphistry/plugins/sentinel_graph.py:437-440, :498-505, :460-463, and :524-531.

Impact: This is non-blocking architecture debt, but the repeated provider-graph normalization logic has already diverged. A small shared helper or documented reserved-column policy would make the F3 fix less ad hoc and reduce drift in future connector work.

F10 — Consider a Sentinel tabular/raw query API if Table response formats stay documented

Status: DEFERRED
Evidence:

  • Kusto exposes tabular results through kql() at graphistry/plugins/kusto.py:204-309 and graph visualization through kusto_graph() at :312-352.
  • Spanner exposes graph visualization through spanner_gql() at graphistry/plugins/spanner.py:297-340 and tabular output through spanner_gql_to_df() at :342-385.
  • Sentinel exposes only graph visualization for queries through sentinel_graph() at graphistry/plugins/sentinel_graph.py:240-288.
  • Sentinel raw bytes are private in _sentinel_graph_query() at graphistry/plugins/sentinel_graph.py:291-331.
  • Sentinel Table response parsing is fallback-only at graphistry/plugins/sentinel_graph.py:445-466 and :510-534.

Impact: This is useful architecture guidance, but after pressure testing it is not a standalone pre-merge requirement. The actionable must-fix is F8: either remove/fix the notebook's raw Table inspection claim or add a public table/raw method. If the PR keeps advertising Table output as inspectable, the Kusto/Spanner split is the cleaner API shape.

Rejected / Non-Findings

  • Credentials: required scans found no obvious secrets or token patterns.
  • GPU/RAPIDS: no changed GPU, Arrow, cuDF, or GFQL runtime path requires DGX validation.
  • Session mutation: configure_sentinel_graph() mutates session state, but this matches existing Kusto plugin behavior, so not a PR-specific regression.
  • Docs/toctree: Sentinel API/notebook is not linked from existing plugin docs pages, but ReadTheDocs passes and the PR includes a notebook. Consider improving discoverability with the changelog/API export fixes; not carried as a merge blocker.

Validation

Commands run:

  • python3 -m pytest -q graphistry/tests/plugins/test_sentinel_graph.py -> fail, 4 auth dependency failures.
  • gh pr checks 1088 --repo graphistry/pygraphistry -> fail, test-minimal-python 3.8/3.14.
  • gh run view 25434926860 --repo graphistry/pygraphistry --job 74610502161 --log -> confirms CI cause.
  • python3 -m compileall -q graphistry/plugins/sentinel_graph.py graphistry/plugins_types/sentinel_graph_types.py -> pass.
  • ./bin/ruff.sh graphistry/plugins/sentinel_graph.py graphistry/plugins_types/sentinel_graph_types.py graphistry/tests/plugins/test_sentinel_graph.py -> pass.
  • git diff --check origin/master...HEAD -> pass.
  • python3 -m json.tool demos/demos_databases_apis/microsoft/sentinel/sentinel_graph_examples.ipynb >/tmp/sentinel_graph_examples_jsoncheck.txt -> pass.
  • Notebook code-cell AST parse -> pass.

Local limitation:

  • python3 -m nbformat --version failed locally because nbformat is not installed; ReadTheDocs/docs checks are passing remotely.

Recommendation

Do not merge PR #1088 yet. Fix F1 before review approval/merge, then address F2-F8 and rerun the minimal CI gate plus focused Sentinel tests. The notebook should also be cleaned up for F9, and F10/F11 are worth handling if the PR keeps the Table response docs or touches graph normalization. After the branch updates, rerun the review skill from this plan and require CI green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants