Repository navigation
v0.6.2: gaps for agents, relationships from inventory, Graph and service principal activity - #13
Merged
Merged
Conversation
… users A mapping part can now read a list column (Part.each): one participant per element, at most 50. IdentityInfo (Defender XDR and Sentinel UEBA) becomes "holds roles": the user with their active Entra roles (target) and PIM-eligible ones (other) as role entities, so who else holds a role is one hop away. DeviceInfo adds the logged-on users already on the board to "reported from public IP" (by on-premises SID, which IdentityInfo adds to users, or DOMAIN\name); unknown users are not created. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XkeMnAg6DWQ8xZucm51CzK
…rface
get_gaps (GET /api/boards/{id}/gaps) lists what the board still needs to
make its entities and relationships clear, most important first, each as a
question with filled-in KQL, the platform to run it on and the mapping
that imports the rows: who a user is and which roles they hold
(IdentityInfo), what a device is and who uses it (DeviceInfo), which app a
service principal is (OAuthAppInfo), where a process came from
(DeviceProcessEvents by start key and device), and the attack impact's
hunts for tables nothing was imported from. Scope: what the attack touches
once something is marked, otherwise every connected entity. Computed in
the browser (web/src/gaps.ts), like every board read.
submit_gap_rows (POST /gaps/{gap_id}/rows) imports the result rows like a
dropped export, keeps rows of no known table in the answer's source, and
records the answer, also an empty one, as a source with the URI
factgraph:gap/<id>: the gap is closed for every browser and agent, and the
reply names the next gap. A gap that changed since get_gaps is refused with
its current ID; rows that fit a table are imported anyway.
get_neighbors (GET /entities/{id}/neighbors) reads one entity's
surroundings up to three hops instead of the whole board.
The agent profile grows to 31 tools (about 10,600 tokens of descriptions);
the server instructions tell agents to work through get_gaps until empty.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XkeMnAg6DWQ8xZucm51CzK
…vity
Imports: AADManagedIdentitySignInLogs, MicrosoftGraphActivityLogs,
AADGraphActivityLogs and Defender's GraphAPIAuditEvents get mappings. Graph
calls are grouped by request with object IDs replaced ("GET
/v1.0/users/{id}/messages"), failed ones apart; the caller is the user (by
object ID) or, for app-only calls, the service principal, with the client
app and, when on the board, the calling device.
Gaps: for each identity the attack touches, what it did in the sources the
board has no rows from for it, as original rows the mappings import: sign-
ins of a service principal or managed identity, AzureActivity by Caller,
Microsoft Graph and the legacy Azure AD Graph calls, from two weeks before
the first evidence. An identity counts as covered for a table once
evidence from it touches the identity, so a dropped export closes the gap
too. The summarising hunts for these (sp-signins, sp-azure, sp-graph) are
no longer offered as gaps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XkeMnAg6DWQ8xZucm51CzK
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XkeMnAg6DWQ8xZucm51CzK
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes FactGraph tell a connected agent precisely what is missing, with the KQL to fetch it, and take the answer through one interface. Also adds the imports that make those answers land in the graph as relationships.
Gaps for agents (b76fa02, c5e38d9)
get_gaps(GET /api/boards/{id}/gaps): what the board still needs, most important first. Each gap has a question, the reason, filled-in KQL, the platform to run it on (Defender advanced hunting or Sentinel) and the mapping that imports the rows. Computed in the browser (web/src/gaps.ts) like every board read.IdentityInfo), what a device is and who uses it (DeviceInfo), which app a service principal is (OAuthAppInfo), where a process came from (DeviceProcessEventsby start key and device).AzureActivityby Caller, Microsoft Graph and the legacy Azure AD Graph. An identity counts as covered once evidence from that table touches it, so a dropped export closes the gap too.submit_gap_rows(POST /gaps/{gap_id}/rows): imports the result rows like a dropped export, keeps rows of no known table in the answer's source, and records the answer (also an empty one, with a note) as a source with the URIfactgraph:gap/<id>. The gap is closed for every browser and agent and the reply names the next gap. A gap that changed sinceget_gapsis refused with its current ID; rows that fit a table are imported anyway.get_neighbors(GET /entities/{id}/neighbors?hops=): one entity's surroundings up to three hops instead of the whole board.get_gapsuntil it is empty.Relationships from inventory (6a482b3)
IdentityInfobecomes holds roles: the user with active and PIM-eligible Entra roles as role entities, so who else holds a role is one hop away.DeviceInfoadds the logged-on users already on the board (joined by the on-premises SID thatIdentityInfoadds); unknown users are not created.Graph and managed identity imports (c5e38d9)
MicrosoftGraphActivityLogs,AADGraphActivityLogs, DefenderGraphAPIAuditEvents,AADManagedIdentitySignInLogs.GET /v1.0/users/{id}/messages); failed calls are kept apart.Release (45a882f)
Version 0.6.2 in the backend, frontend, publish script and README.
Tests
tests/gaps.cjs(every gap kind, coverage by evidence and by answers, reopening for new entities, neighbourhood); format checks, build, performance budgets.get_gaps→submit_gap_rowswith anIdentityInforow → roles on the board → empty answer → remaining activity gaps →get_neighbors→ stale gap refused) failed in that run on an outdated gap count; after updating its expectations it passes on its own.Generated by Claude Code