Skip to main content

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-paths internally — 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)

FieldTypeDescription
workload_idstringEntity ID of the automation artifact
identity_idstring | nullEntity ID of the execution identity (null if unbound)
destination_idstringEntity ID of the destination resource
data_domainstringBusiness domain of the destination
sensitivitystringpublic, internal, confidential, restricted
via_rolesstring[]Human-readable role names granting the authority
via_role_idsstring[]Corresponding role entity IDs
actionsstring[]Observed actions (e.g., ReadData, WriteData)
source_systemstringConnector that produced this path
auth_chain_depthnumberNumber of hops in the authority chain
path_lineage_idstringGroups path versions across scans (stable across updates)
composition_hashstringHash of (identity, roles, actions) for change detection
statusstringactive or removed
first_seen_atDateWhen this path was first materialized
last_seen_atDateWhen this path was last confirmed by a scan

current_state (nested object, updated each evaluation run):

FieldTypeDescription
execution_30dnumberObserved executions in the last 30 days
prior_execution_30dnumber | nullExecutions in the prior 30-day window (for trend)
last_execution_atDate | nullTimestamp of most recent observed execution
ownership_statusstringowned, orphaned, ambiguous, unknown
egress_categorystringinternal, external, llm
active_finding_countnumberCount of non-suppressed active findings
max_finding_severitystring | nullHighest 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:

ParameterTypeDescription
limitnumberPage size (default 50, max 200)
cursorstringPagination cursor from previous response
sortstringSort field: _id, first_seen_at, last_seen_at, execution_30d
workload_idstringFilter by workload entity ID
identity_idstringFilter by identity entity ID
data_domainstringFilter by data domain
sensitivitystringFilter by sensitivity level
statusstringFilter by path status
egress_categorystringinternal, external, llm
ownership_statusstringowned, orphaned, ambiguous, unknown
has_findingsbooleantrue = only paths with active findings
finding_typestringComma-separated finding types (AND match)
qstringFree-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_type filter is an AND match — scope_drift,llm_egress returns only paths with both finding types active simultaneously
  • Entity display names and source systems are enriched server-side via batch lookup
  • finding_types is 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_name or "Not assigned"
  • Runtime identity owner: identity entity's owner_name or 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_AS or 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:

PatternEdgeIdentity AssignmentDepth
Direct rolesWorkload HAS_ROLE → Role → Resourceidentity_id = null (unbound)0
Identity bindingWorkload RUNS_AS → Identity → Role → Resourceidentity_id = target identity0
Chain forwardingCALLS / INVOKES / USES (transparent)Inherited from chain sourceSame
Cross-system authIdentity 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:

Pathidentity_iddestinationauth_chain_depth
Aid-svc-finance (Entra SP)financial-api0
Bid-sn-int-finance (SN user)hr-employee-db1

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

MetricMethodRationale
max_execution_30dMAX across pathsSame execution evidence is stamped on every path from a workload; SUM would overcount
active_finding_countSUM across pathsFindings ARE distinct per path (keyed by path_id + finding_type)
max_finding_severityWorst-casecritical > high > medium > low
ownership_statusWorst-caseorphaned > unknown > ambiguous > owned
combined_rolesSet unionDeduplicated role names across all paths
combined_actionsSet unionDeduplicated action names across all paths
last_execution_atMAXMost 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:

KeySurfaces (demo)Cross-workload visible?Trade-off
identity_id~13Yes — surfaces show multiple workloadsLoses workload-level scoping
(workload_id, identity_id)~17No — same identity split across workloadsPreserves workload context
workload_id~12N/A — groups everything per workloadLoses 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:

  1. Phase 1 (planned): Virtual grouping — computed at query time, no new storage
  2. Phase 2 (possible future): Materialized surfaces — IdentityAccessSurfaceDoc as a stored collection, updated during path materialization. Same API contract.
  3. 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.