Access Paths
An access path is the complete, observable chain through which a workload performs autonomous actions against a destination resource — without human involvement at execution time.
Terminology note: The user-facing term is Access Path. Use "Execution Access Path" only when formal precision is necessary. The codebase still uses
AuthorityPathDoc,AuthorityPathDetailPage, and/api/v1/authority-pathsinternally — these are synonyms referring to the same concept. The internal naming predates the terminology standardization and will be migrated incrementally.
Concept
What is an Access Path?
A traditional access review asks: "What can this user do?" An access path asks: "What is this automation actually doing, and how did it get that authority?"
Each path represents a distinct combination of:
Workload → [Identity] → Destination → Data Domain
│ │ │
│ (optional) the resource sensitivity level
│ execution being accessed (restricted,
│ identity confidential, etc.)
│
source artifact: Lambda, ServiceNow Flow, Azure Logic App, etc.
- Workload: The automation artifact (Lambda function, ServiceNow scheduled job, etc.)
- Identity: The managed identity or service principal the workload runs as (optional — some workloads bind directly)
- Destination: The resource being accessed (database, API, storage account, etc.)
- Data Domain: The business domain of the destination (customer data, finance, HR, etc.)
The path is derived from observed execution traces — actual calls seen by the connector — not from role assignments alone. A path exists because the workload was observed exercising the authority, not merely because it theoretically could.
CISO-Facing Explanation
An access path answers: "Confirm that this automated process is supposed to have this access, and that someone owns it."
Key risk signals on a path:
- Active findings: Detected anomalies (scope drift, orphaned ownership, etc.)
- Execution count: How many times the path was exercised in the last 30 days — determines real-world impact
- Ownership status: Whether a named person or team is accountable for the workload
- Sensitivity: How sensitive the destination data is
Data Model
AuthorityPathDoc (key fields)
| Field | Type | Description |
|---|---|---|
workload_id | string | Entity ID of the automation artifact |
identity_id | string | null | Entity ID of the execution identity (null if unbound) |
destination_id | string | Entity ID of the destination resource |
data_domain | string | Business domain of the destination |
sensitivity | string | public, internal, confidential, restricted |
via_roles | string[] | Human-readable role names granting the authority |
via_role_ids | string[] | Corresponding role entity IDs |
actions | string[] | Observed actions (e.g., ReadData, WriteData) |
source_system | string | Connector that produced this path |
auth_chain_depth | number | Number of hops in the authority chain |
path_lineage_id | string | Groups path versions across scans (stable across updates) |
composition_hash | string | Hash of (identity, roles, actions) for change detection |
status | string | active or removed |
first_seen_at | Date | When this path was first materialized |
last_seen_at | Date | When this path was last confirmed by a scan |
current_state (nested object, updated each evaluation run):
| Field | Type | Description |
|---|---|---|
execution_30d | number | Observed executions in the last 30 days |
prior_execution_30d | number | null | Executions in the prior 30-day window (for trend) |
last_execution_at | Date | null | Timestamp of most recent observed execution |
ownership_status | string | owned, orphaned, ambiguous, unknown |
egress_category | string | internal, external, llm |
active_finding_count | number | Count of non-suppressed active findings |
max_finding_severity | string | null | Highest severity among active findings |
REST API
Base path: /api/v1/authority-paths
All endpoints require X-Tenant-Id header.
GET /api/v1/authority-paths — List paths
Returns a cursor-paginated list of access paths with entity enrichment and finding summary.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
limit | number | Page size (default 50, max 200) |
cursor | string | Pagination cursor from previous response |
sort | string | Sort field: _id, first_seen_at, last_seen_at, execution_30d |
workload_id | string | Filter by workload entity ID |
identity_id | string | Filter by identity entity ID |
data_domain | string | Filter by data domain |
sensitivity | string | Filter by sensitivity level |
status | string | Filter by path status |
egress_category | string | internal, external, llm |
ownership_status | string | owned, orphaned, ambiguous, unknown |
has_findings | boolean | true = only paths with active findings |
finding_type | string | Comma-separated finding types (AND match) |
q | string | Free-text search |
Response:
{
"data": [
{
"_id": "path-123",
"workload": { "id": "wl-1", "display_name": "my-function", "source_system": "aws_lambda" },
"identity": { "id": "id-1", "display_name": "service-principal", "source_system": "azure_ad" },
"destination": { "id": "db-1", "display_name": "prod-db", "source_system": "postgres" },
"data_domain": "customer_data",
"sensitivity": "restricted",
"via_roles": ["AzureDBReader", "AzureDBWriter"],
"execution_30d": 42,
"ownership_status": "owned",
"egress_category": "internal",
"active_finding_count": 2,
"max_finding_severity": "high",
"finding_types": ["scope_drift", "dormant_authority"],
"status": "active"
}
],
"cursor": { "next": "abc...", "has_more": true },
"meta": { "total_count": 247 }
}
Notes:
- The
finding_typefilter is an AND match —scope_drift,llm_egressreturns only paths with both finding types active simultaneously - Entity display names and source systems are enriched server-side via batch lookup
finding_typesis a set of all active finding types on the path (computed server-side)
GET /api/v1/authority-paths/:id — Single path detail
Returns full AuthorityPathDoc including all fields. Used by AuthorityPathDetailPage.
Response:
{
"data": {
"_id": "path-123",
"tenant_id": "tenant-xyz",
"path_lineage_id": "pl-456",
"workload_id": "wl-1",
"identity_id": "id-1",
"destination_id": "db-1",
"data_domain": "customer_data",
"sensitivity": "restricted",
"via_roles": ["AzureDBReader"],
"via_role_ids": ["role-id-1"],
"actions": ["ReadData", "WriteData"],
"source_system": "aws_lambda",
"auth_chain_depth": 2,
"current_state": {
"execution_30d": 42,
"last_execution_at": "2026-01-15T11:45:00Z",
"ownership_status": "owned",
"egress_category": "internal",
"active_finding_count": 2,
"max_finding_severity": "high"
},
"first_seen_at": "2025-12-01T00:00:00Z",
"last_seen_at": "2026-01-15T12:30:00Z",
"status": "active"
}
}
GET /api/v1/authority-paths/:id/findings — Path findings
Returns findings associated with this path, paginated.
Query parameters: limit, cursor, status, severity
Response:
{
"data": [
{
"_id": "finding-456",
"finding_type": "scope_drift",
"severity": "high",
"status": "active",
"detected_at": "2026-01-10T15:00:00Z",
"explanation": "Entity gained 2 new roles since baseline (2025-12-01): AzureDBWriter, StorageBlobReader. StorageBlobReader reaches restricted data domain 'customer_data'.",
"evidence_refs": {
"added_roles": ["AzureDBWriter", "StorageBlobReader"],
"sensitive_domains": ["customer_data"],
"drift_categories": ["HAS_ROLE"],
"baseline_version_date": "2025-12-01"
},
"intervals": [
{ "start": "2026-01-10T15:00:00Z", "end": null, "resolution_reason": null }
]
}
],
"cursor": { "next": null, "has_more": false },
"meta": { "total_count": 2 }
}
intervals: Each interval represents a period when the finding was active. An open end: null means the finding is still active. A closed interval includes resolution_reason (e.g., "role_removed", "owner_restored").
GET /api/v1/authority-paths/:id/remediation — Remediation guidance
Returns prioritized remediation actions. See Drift Evaluator Framework — Remediation Service for how actions are generated and scored.
Response:
{
"data": {
"path_id": "path-123",
"actions": [
{
"action": "Remove role 'StorageBlobReader' from the workload identity",
"rationale": "Reduces authority scope to baseline; StorageBlobReader grants access to restricted customer data",
"impact_score": 9,
"finding_type": "scope_drift"
},
{
"action": "Restrict access to the 'customer_data' domain",
"rationale": "Prevents newly reachable sensitive destinations from being exercised",
"impact_score": 7,
"finding_type": "reachability_drift"
}
],
"generated_at": "2026-03-08T14:20:00Z"
}
}
Access Path Detail Page
The detail page (ui/src/pages/AuthorityPathDetailPage.tsx) provides a comprehensive, interactive view of a single access path.
Layout (top to bottom)
1. Header
- Workload → Identity (optional) → Destination breadcrumb
- Subtitle: "Autonomous access path actively executing against {domain} data under {ownership_status} ownership"
- Last evidence verification timestamp
2. Access Path Diagram
- Interactive graph: workload node → identity node (optional) → destination node → data domain node
- Role names shown as edge labels
- Click any node to open the Entity Detail drawer (right sidebar)
3. Active Risk Conditions
- Expandable finding tiles, one per active finding
- Each tile: finding type label, severity badge, status badge
- Expand to see: explanation text, detection date, active intervals with resolution reasons
- Right side: compact execution stats (last execution timestamp, 30-day execution count)
4. Remediation Guidance
- Numbered list of up to 5 actions from the remediation service
- Each action: action text, finding type badge(s), rationale, impact score bar (red ≥ 8, amber ≥ 5, blue < 5)
- Shows "No active findings" message when path is clean
5. Ownership Section
- Automation owner: workload's
owner_nameor "Not assigned" - Runtime identity owner: identity entity's
owner_nameor red error if the owner has departed
6. Automation Metadata
- Source system badge, artifact identifier (monospace source ID), last refresh time
7. Identity Binding
- Relationship type (
RUNS_ASor Unknown), auth protocol (OIDC Federated), target system
8. Access Authority State
- Execution model (Autonomous), auth type (Client credentials), human session required (No)
- Collapsible: role pills (each linked to entity detail page), action pills
Path Materialization — From Entity Graph to Access Paths
Access paths are not stored directly by connectors. They are materialized from the entity graph during ingestion. Understanding this process is essential for understanding what identity_id means on a path and how grouping works.
Graph Traversal
The path materializer walks outward from each workload entity, following relationship edges:
Four traversal patterns produce paths:
| Pattern | Edge | Identity Assignment | Depth |
|---|---|---|---|
| Direct roles | Workload HAS_ROLE → Role → Resource | identity_id = null (unbound) | 0 |
| Identity binding | Workload RUNS_AS → Identity → Role → Resource | identity_id = target identity | 0 |
| Chain forwarding | CALLS / INVOKES / USES (transparent) | Inherited from chain source | Same |
| Cross-system auth | Identity AUTHENTICATES_TO → Identity (depth+1) | identity_id = target identity | +1 |
The identity_id Assignment Rule
identity_id is always the identity that directly holds the roles granting access to the destination. It is NOT the workload, and it is NOT the first identity in the chain.
Example — cross-system chain:
This produces two paths with different identity_id values:
| Path | identity_id | destination | auth_chain_depth |
|---|---|---|---|
| A | id-svc-finance (Entra SP) | financial-api | 0 |
| B | id-sn-int-finance (SN user) | hr-employee-db | 1 |
Path B's identity_id is the ServiceNow user, not the Entra SP — because the SN user is the one that holds hr-itil role.
Path Merging
After traversal, paths with the same (resource_id, via_identity) are merged. Multiple roles/actions reaching the same destination through the same identity become one path with combined via_roles and actions arrays. The path ID formula:
path_id = SHA256(tenant_id : workload_id : identity_id : destination_id)[:24]
lineage = SHA256(tenant_id : workload_id : destination_id)[:24]
Execution Evidence Attribution
Execution evidence (ExecutionEvidenceDoc) is recorded at the workload level, not per-path. The materializer stamps the same execution_30d count on every path from a given workload. This is why grouped metrics must use MAX (not SUM) for execution — summing would count the same evidence multiple times.
Identity-Scoped Grouping (Access Surfaces) — Proposed
Status: Proposed design, not yet shipped. The sections below describe the planned identity-scoped grouping feature. The live platform currently exposes only the flat access path endpoints documented above. See Access Path Grouping Research for the full analysis and implementation phases.
The Problem: Path Noise
In the flat view, each (workload, identity, destination) combination is a separate row. For identities that reach many destinations or are shared across workloads, this creates noise:
The real story is simpler: one identity reaches 5 financial systems via 5 roles. The product direction is for this to become one remediation conversation, not five — though identity-scoped remediation deduplication requires additional design beyond the initial grouped view (see research Phase 4).
The Solution: Identity Access Surfaces
An Identity Access Surface groups all paths sharing the same identity_id into a single collapsible row:
Cross-Workload Identity Sharing
When one identity is used by multiple workloads (via separate RUNS_AS edges), the grouped view collapses them into a single surface showing all workloads:
Flat view: 5 rows (3 from agent + 2 from provisioner)
Grouped view: 1 surface
▸ svc-foundry (via Foundry Agent, Foundry Provisioner)
4 roles → 5 destinations │ ⚠ Shared across 2 workloads │ Orphaned │ 🔶 High
This is a higher-risk pattern — compromise of id-svc-foundry affects both workloads. The flat view hides this by splitting the rows.
Unbound Paths
Workloads with no RUNS_AS relationship have identity_id = null. These are grouped by workload_id instead:
▸ [Unbound] CRM Sync
2 roles → 2 destinations │ Unknown ownership │ External egress
Cross-System Chains in Grouped View
When a workload reaches destinations through multiple identities (via AUTHENTICATES_TO), each identity becomes its own surface:
The cross-system relationship is visible through auth_chain_depth on child paths within each surface. A future enhancement could annotate surfaces with their upstream auth chain.
Aggregation Rules
| Metric | Method | Rationale |
|---|---|---|
max_execution_30d | MAX across paths | Same execution evidence is stamped on every path from a workload; SUM would overcount |
active_finding_count | SUM across paths | Findings ARE distinct per path (keyed by path_id + finding_type) |
max_finding_severity | Worst-case | critical > high > medium > low |
ownership_status | Worst-case | orphaned > unknown > ambiguous > owned |
combined_roles | Set union | Deduplicated role names across all paths |
combined_actions | Set union | Deduplicated action names across all paths |
last_execution_at | MAX | Most recent activity across any path |
IdentityAccessSurface Type
interface IdentityAccessSurface {
identity: { id: string; display_name: string; source_system: string } | null;
workloads: Array<{ id: string; display_name: string }>;
destinations: Array<{
id: string;
display_name: string;
data_domain: string;
sensitivity: string;
}>;
combined_roles: string[];
combined_actions: string[];
paths: AuthorityPathListItem[]; // embedded child paths for drill-in
path_count: number;
max_execution_30d: number;
last_execution_at: string | null;
ownership_status: string;
max_finding_severity: string | null;
finding_types: string[];
active_finding_count: number;
cross_workload: boolean;
}
Proposed API: Grouped Endpoint
Not yet implemented. The endpoint path, pagination model, and response shape below are proposed and subject to change during implementation. The existing flat endpoint is unaffected.
The proposed approach is a separate endpoint (e.g., GET /api/v1/authority-paths/grouped) rather than a query parameter on the existing endpoint, because the response shape and pagination model differ. The existing architecture convention is cursor-based pagination for all list endpoints (see API Layer); grouped surfaces have variable child counts which may require offset pagination instead. This trade-off will be resolved during implementation.
Query parameters: Same filters as the flat endpoint + pagination params (TBD: offset or cursor).
Filter semantics: Filters apply to flat paths BEFORE grouping. A data_domain=hr filter shows only surfaces containing HR paths, with metrics computed from the filtered subset only.
Response:
{
"data": [
{
"identity": { "id": "id-svc-finance", "display_name": "svc-finance", "source_system": "entra_id" },
"workloads": [{ "id": "wl-invoice-rule", "display_name": "Invoice Rule Engine" }],
"destinations": [
{ "id": "res-financial-api", "display_name": "Financial API", "data_domain": "financial", "sensitivity": "restricted" },
{ "id": "res-invoice-archive", "display_name": "Invoice Archive", "data_domain": "financial", "sensitivity": "confidential" }
],
"combined_roles": ["finance-read", "invoice-view", "ap-write", "ar-write", "ledger-admin"],
"paths": [ ... ],
"path_count": 5,
"max_execution_30d": 847,
"last_execution_at": "2026-03-25T14:30:00Z",
"ownership_status": "owned",
"max_finding_severity": "high",
"finding_types": ["scope_drift", "reachable_sensitive_domain"],
"active_finding_count": 10,
"cross_workload": false
}
],
"meta": {
"total_surfaces": 13,
"total_paths": 76,
"offset": 0,
"limit": 50,
"has_more": false,
"group_key": "identity"
}
}
Graph Explorer Integration (Future)
The graph explorer could support identity-scoped filtering:
- Ungrouped: Show all nodes and edges as today
- Filter by surface: Select an
IdentityAccessSurface→ highlight the identity node, its workload(s), role(s), and destination(s) in the graph, dimming everything else - Grouped overlay: Color-code nodes by which surface they belong to, showing how surfaces overlap at shared destinations
This is not in Phase 1 scope but the data model supports it — each surface carries path_ids and embedded child paths with full entity references.
Grouping Key Decision
The grouping key is a product decision, not an implementation detail:
| Key | Surfaces (demo) | Cross-workload visible? | Trade-off |
|---|---|---|---|
identity_id | ~13 | Yes — surfaces show multiple workloads | Loses workload-level scoping |
(workload_id, identity_id) | ~17 | No — same identity split across workloads | Preserves workload context |
workload_id | ~12 | N/A — groups everything per workload | Loses identity-level risk signal |
Current recommendation: identity_id — aligns with Sergey's focus on "one identity carrying multiple roles" as the unit of risk.
The implementation supports changing this key without schema changes — grouping is computed at query time, not materialized.
Path to Model Evolution
The grouping design deliberately keeps AuthorityPathDoc as the canonical primitive:
- Phase 1 (planned): Virtual grouping — computed at query time, no new storage
- Phase 2 (possible future): Materialized surfaces —
IdentityAccessSurfaceDocas a stored collection, updated during path materialization. Same API contract. - Phase 3 (possible future): Surface-level findings and remediation — evaluator rules that operate on surfaces rather than individual paths
Each phase builds on the previous. Phase 1 validates whether identity-scoped grouping is the right axis before committing to permanent storage.
Related
- Drift Evaluator Framework — how findings are produced and how the remediation service works
- API Layer — general API conventions (pagination, error format, authentication)
- Data Model — full entity and finding schemas
- Access Path Grouping Research — noise analysis, option comparison, and implementation phases