Skip to main content

Entity Type Classification — Architect

Role: Systems Architect Date: 2026-02-13 (Round 5) Core Question: What entity type should Business Rules, Script Includes, REST Messages, OAuth Profiles, Flow Designer Flows, and Scheduled Jobs actually be?


Executive Summary

The founder is right. The current model is semantically wrong.

A Business Rule does not authenticate. A Script Include does not hold credentials. A REST Message template is a configuration artifact. An OAuth Profile is credential infrastructure. None of these are identities. Only the Service Principal is truly an identity — something that authenticates, holds authorization grants, and appears as an actor in audit logs.

Rounds 1-4 of this analysis skirted the fundamental question by recommending "keep entity_type: identity and fix labels in the UI." That was a pragmatic recommendation driven by migration cost concerns. The founder has explicitly removed that constraint: there are no active clients, and the model can be rewritten from scratch.

This document takes that freedom seriously. It designs the entity type system from first principles, applying a rigorous identity test to every artifact in the execution chain, and proposes a type hierarchy that is semantically correct, architecturally pure, and future-proof.

Key conclusions:

  1. The platform needs 9 entity types, not 7. The current autonomous_identity type is a grab-bag that conflates four fundamentally different concepts: identities that authenticate, automation logic that executes, connection configurations that define outbound targets, and credential material that enables authentication.

  2. Every artifact in the ServiceNow-Entra chain gets a definitive classification based on a three-part identity test: Does it authenticate? Does it hold its own authorization? Does it appear as an actor in logs?

  3. The RUNS_AS relationship becomes the critical bridge between automations and identities, replacing the current conflation. An automation RUNS AS an identity; it IS NOT an identity.

  4. Edge types require significant restructuring. AUTHENTICATES_TO no longer connects two "identity" nodes of which one is a Business Rule. Instead, it connects two genuine identities, and automation-to-identity delegation flows through RUNS_AS.

  5. The corrected graph structure for the AzureGraphRouter chain is cleaner, more honest, and more queryable than the current model.


Table of Contents

  1. Why the Current Model Is Wrong
  2. The Identity Test
  3. First-Principles Type Hierarchy
  4. Per-Artifact Classification
  5. Edge Type Implications
  6. The RUNS_AS vs IS Distinction
  7. NormalizedNodeType Enum Proposal
  8. Corrected Graph Structure
  9. Impact on Existing Architecture
  10. Impact on Path Materialization
  11. Impact on Findings and Evaluator
  12. Impact on OAA Export
  13. Counter-Arguments and Rebuttals
  14. Migration Path
  15. Appendix A: Full Artifact Classification Table
  16. Appendix B: Edge Type Reference
  17. Appendix C: Query Pattern Changes

1. Why the Current Model Is Wrong

The Conflation Problem

The current NormalizedNodeType enum has 7 values:

type NormalizedNodeType =
| "autonomous_identity"
| "human_identity"
| "role"
| "permission"
| "resource"
| "credential"
| "execution_evidence";

All of the following are classified as autonomous_identity:

ArtifactWhat It Actually Is
Service Principal sn-ticket-routerAn identity that authenticates via OAuth, holds Entra role assignments, appears in sign-in logs
Business Rule "Auto-route identity tickets"A configuration artifact that defines trigger conditions and a script to execute
Script Include AzureGraphRouterA reusable code library that is invoked by other artifacts
Flow Designer Flow "HR Onboarding"A visual workflow configuration that defines execution steps
Scheduled Job "Nightly CMDB Sync"A timer configuration that triggers periodic execution

These five things have almost nothing in common. The Service Principal authenticates to Entra ID, holds Application.ReadWrite.All permissions, and appears in Azure sign-in logs. The Business Rule is a JavaScript snippet stored in sys_script that fires when an incident is inserted. The Script Include is a reusable class definition that provides helper methods. The Flow Designer Flow is a visual workflow definition. The Scheduled Job is a cron-like timer.

Calling all of them "identity" is like calling a car, a road, a traffic light, a driver's license, and a gas station all "vehicles" because they all participate in transportation.

The Semantic Damage

This conflation causes concrete harm in the platform:

1. Path materialization lies. The path materializer at sv0-platform/src/ingestion/path-materializer.ts starts from nodes with entity_type === "identity" and traverses HAS_ROLE -> GRANTS -> APPLIES_TO. But a Business Rule does not HAS_ROLE — it does not hold role assignments. It executes in the context of whatever identity the platform's execution engine assigns to it. The path materializer currently either (a) skips automation nodes because they have no HAS_ROLE edges, producing incomplete paths, or (b) follows RUNS_AS edges to reach an identity, effectively doing the right thing by accident but labeling it incorrectly.

2. Findings are imprecise. When the evaluator detects orphaned_ownership on an automation node, the finding says "Service principal has no active owner." But it is not a service principal. It is a Business Rule. The service principal it delegates to may have a perfectly active owner. The Business Rule's own creator may have departed, but the underlying identity (the SP) may be well-managed. The current model conflates these two ownership concerns.

3. AUTHENTICATES_TO is semantically broken. The AUTHENTICATES_TO edge is defined as connecting an identity in one system to an identity in another system. Currently, a Business Rule is an "identity," so the graph shows: Business Rule -[AUTHENTICATES_TO]-> Service Principal. But a Business Rule does not authenticate. It invokes a Script Include, which calls a REST Message, which uses an OAuth Profile that was configured with a client_id/secret pair that authenticates as the Service Principal. The current model skips four intermediate steps and draws a false direct line.

4. Blast radius calculations are inflated. If the platform counts "identities with access to HR data," it includes Business Rules, Script Includes, and Scheduled Jobs alongside actual service principals and machine accounts. This produces an inflated count that misrepresents the threat surface. A CISO sees "47 identities accessing HR data" when the truth is "3 service principals accessing HR data, each invoked by 5-15 automation configurations."

5. The CISO cannot distinguish threat types. "An identity has dormant authority" means something very different for a Service Principal (which holds real credentials that could be compromised) versus a Business Rule (which is a configuration artifact that only executes when its trigger fires). The current model forces the CISO to mentally decode identitySubtype on every finding to determine the actual risk profile.

Why Rounds 1-4 Got This Wrong

Rounds 1-4 recommended keeping entity_type: identity for a defensible reason: migration cost. The path materializer, evaluator, storage adapter, API routes, and UI all branch on entity type. Changing the type of 80+ nodes from identity to something else would touch every layer of the stack.

But the founder has removed that constraint. With zero active clients and freedom to rewrite, the question is not "what is cheapest to maintain?" but "what is semantically correct?" The answer is clear: these artifacts are not identities.


2. The Identity Test

An entity qualifies as an identity if and only if it satisfies at least two of the following three criteria:

Criterion 1: Authentication

Does this entity authenticate to a system using credentials it owns or is bound to?

  • A Service Principal authenticates via OAuth client credentials, certificates, or federated identity. YES.
  • A Business Rule fires when a database event occurs. It does not present credentials to anything. NO.
  • A Script Include is a library class instantiated by other code. It does not authenticate. NO.
  • A REST Message template defines an HTTP endpoint and headers. It does not authenticate on its own — it holds a reference to an OAuth Profile or credential that does. NO.
  • An OAuth Profile stores client_id, client_secret, token endpoint, and scopes. It is credential infrastructure, not an authenticating entity. NO.
  • A Flow Designer Flow is configured with a "Run As" user but does not present credentials itself. The platform's flow engine authenticates on its behalf. NO (but close).
  • A Scheduled Job runs as a configured user but does not present credentials. The scheduler authenticates on its behalf. NO (but close).

Criterion 2: Authorization

Does this entity hold its own authorization grants (roles, permissions, scopes) that are assignable and revocable?

  • A Service Principal holds Entra role assignments (e.g., Application.ReadWrite.All) and appears in sys_user_has_role for its ServiceNow counterpart. YES.
  • A Business Rule does not have role assignments. It inherits the execution context of the system or the user-session that triggered it. NO.
  • A Script Include does not have role assignments. NO.
  • A REST Message template does not have role assignments. NO.
  • An OAuth Profile defines requested scopes but does not hold assignable roles. NO.
  • A Flow Designer Flow may have a run_as configuration, but this borrows another identity's roles — it does not have its own role assignments. PARTIAL — borrows, does not own.
  • A Scheduled Job may run as a specific user, borrowing that user's roles. PARTIAL — borrows, does not own.

Criterion 3: Actor in Audit Logs

Does this entity appear as the actor or initiated_by entity in audit logs?

  • A Service Principal appears in Azure sign-in logs as the authenticating principal and in ServiceNow syslog_transaction as the user context. YES.
  • A Business Rule does not appear as an actor. The syslog_transaction shows the user context (often "system" or the triggering user), not the Business Rule's sys_id. NO.
  • A Script Include does not appear as an actor. NO.
  • A REST Message does not appear as an actor. The outbound HTTP call is logged, but the actor is the user context, not the REST Message template. NO.
  • An OAuth Profile does not appear as an actor. NO.
  • A Flow Designer Flow appears in sys_flow_context as the executing flow, but the actor in syslog_transaction is the flow's run-as user, not the flow itself. PARTIAL.
  • A Scheduled Job appears in sys_trigger logs as the executing job, but the actor in syslog_transaction is the job's run-as user. PARTIAL.

The Verdict

ArtifactAuthenticatesHas Own AuthZActor in LogsScoreIdentity?
Service PrincipalYESYESYES3/3YES
Application Registration (Entra)YESYESYES3/3YES
Machine Account (ServiceNow sys_user)YESYESYES3/3YES
OAuth App (Entra)YESYESYES3/3YES
Flow Designer FlowNOPARTIALPARTIAL1/3NO
Scheduled JobNOPARTIALPARTIAL1/3NO
Business RuleNONONO0/3NO
Script IncludeNONONO0/3NO
REST Message (template)NONONO0/3NO
REST Message (endpoint/method)NONONO0/3NO
OAuth Provider (config)NONONO0/3NO
OAuth ProfileNONONO0/3NO
Role AssignmentN/AN/AN/AN/ANO (it is a role)
Permission/ScopeN/AN/AN/AN/ANO (it is a permission)
Incident TableN/AN/AN/AN/ANO (it is a resource)
Graph API EndpointN/AN/AN/AN/ANO (it is a resource)

The line is clear. Only entities that can authenticate, hold their own authorization, and appear as actors qualify as identities. Everything else has a different fundamental nature.

The "PARTIAL" Cases: Flows and Scheduled Jobs

Flows and Scheduled Jobs are the most interesting edge cases. They have some identity-like characteristics:

  • They appear in execution context records (sys_flow_context, sys_trigger)
  • They have configurable run_as settings that determine their effective authority
  • They can be enabled/disabled, which affects execution capability
  • They are the "unit of deployment" for automation — a CISO wants to see "which flows are running"

But they fail the identity test because:

  • They do not authenticate. The ServiceNow platform authenticates on their behalf using the run_as user's session.
  • They do not hold their own roles. They borrow the run_as user's role set.
  • In audit logs, the actor is the run_as user, not the flow or job itself.

The correct classification is automation. They are automation configurations that delegate to identities via RUNS_AS. The fact that they have some actor-like properties (contextual logging, enable/disable state) is captured by the automation type's properties, not by promoting them to identity status.


3. First-Principles Type Hierarchy

The Question

In an execution exposure management domain, what are the fundamental categories of things we track?

Start from the domain, not from the current codebase. What kinds of entities exist in the problem space?

Category Analysis

Things that act (identities). A Service Principal authenticates to Entra ID, receives an OAuth token, and makes API calls to the Microsoft Graph. A machine account authenticates to ServiceNow and executes REST API calls. These entities have credentials, hold role assignments, and appear as actors in audit logs. They ARE the execution authority.

Things that define execution logic (automations). A Business Rule defines "when an incident is inserted, run this JavaScript." A Flow Designer Flow defines "step 1: query user, step 2: create ticket, step 3: send email." A Scheduled Job defines "every night at 2 AM, run this script." These entities are configuration artifacts. They define WHAT executes and WHEN, but they are not the AUTHORITY under which execution occurs. They delegate to identities.

Things that define outbound targets (connections). A REST Message template defines "the Azure Graph API is at https://graph.microsoft.com, and we use this OAuth profile to authenticate." A REST Message function/method defines "to get a user, call GET /users/{id}." These are connection configurations — they define WHERE outbound calls go and HOW to authenticate, but they are neither the identity doing the calling nor the resource being called.

Things that enable authentication (credentials). An OAuth Profile stores client_id, client_secret, token endpoint, and scopes. A certificate provides X.509 mutual TLS authentication material. An API key provides static bearer token authentication. These are authentication enablers — they are USED BY identities to authenticate. They are not identities themselves.

Things that receive actions (resources). The incident table receives INSERT operations. The Microsoft Graph API receives GET/POST requests. The hr_case table contains sensitive data. These are the targets of execution — the things that identities act upon.

Things that group capabilities (roles). The itil role groups permissions for incident management. The Application.ReadWrite.All scope bundles Graph API capabilities. These are authorization containers.

Things that represent individual capabilities (permissions). The incident.write ACL grants update access to the incident table. The User.Read.All scope grants read access to users. These are atomic authorization grants.

Things that prove execution (execution evidence). A sign-in log entry proves the SP authenticated. A syslog_transaction entry proves the machine account made an API call. A sys_flow_context entry proves the flow executed. These are immutable proof records.

Things that are accountable (owners). Note: owners are handled by the platform's normalizer layer from human_identity nodes. This is already correct in the current design and does not need to change.

The Nine Categories

From this analysis, 9 fundamental node types emerge (including the 2 that already exist correctly):

#TypeWhat It RepresentsExamples
1identitySomething that authenticates and actsService Principal, Machine Account, OAuth App, GitHub App
2automationConfiguration that defines execution logicBusiness Rule, Script Include, Flow Designer Flow, Scheduled Job
3connectionOutbound target configurationREST Message, HTTP endpoint definition, SOAP endpoint
4credentialAuthentication materialOAuth Profile, Client Secret, Certificate, API Key
5roleGrouping of permissionsEntra role, ServiceNow role, AWS IAM policy
6permissionIndividual capabilityACL entry, Graph API scope, S3 action
7resourceThing being acted uponTable, API endpoint (target), Repository, S3 bucket
8execution_evidenceProof of executionSign-in log, transaction log, flow context
9human_identityHuman user account (for ownership derivation)Entra user, ServiceNow sys_user

Why automation Instead of More Granular Types

One might argue for separating automation into finer types: trigger_automation (BR), library (SI), workflow (Flow), scheduled_automation (Job). The argument for a single automation type is:

  1. They all participate in the same graph position. Automations sit between triggers (resources/events) and identities. The graph pattern is always: Resource -[trigger]-> Automation -[delegates to]-> Identity -[has auth]-> Role -> Permission -> Resource. Whether the automation is a BR, SI, Flow, or Job does not change this structural position.

  2. They share the same properties. All automations have: trigger conditions, execution context, enable/disable state, creator, last-modified date, script content (or flow definition). The differences are captured by automationSubtype, not by different entity types.

  3. The UI needs subtype discrimination, not type discrimination. The display layer needs to show "Business Rule" vs "Flow" badges. This is handled by a automationSubtype property, exactly as identitySubtype handles "service_principal" vs "oauth_app" today.

  4. Query patterns are the same. "Show me all automations in this execution chain" is a single query regardless of subtype. "Show me all Business Rules" is a filter on automationSubtype, which works within a single entity type.

Why connection Instead of Making REST Messages Resources

REST Messages are not resources. A resource is something that receives actions — the incident table, the Graph API, the S3 bucket. A REST Message template is the platform's internal representation of HOW to reach an external resource — it defines the URL, authentication method, and HTTP parameters for an outbound connection.

The distinction matters because:

  • Resources appear in blast radius calculations. "This identity can reach the incident table" is meaningful. "This identity can reach the REST Message template" is not — the template is not a target, it is plumbing.
  • Resources have sensitivity classifications. The incident table is internal. The hr_case table is confidential. A REST Message template does not have sensitivity — it is an intermediary.
  • Connections carry authentication configuration. A REST Message is bound to an OAuth Profile or API key. This authentication_method property is specific to connections, not resources.
  • Connections are directional. A connection points outward from the source system to an external target. A resource is the target itself.

In the corrected model, the REST Message (connection) POINTS TO a resource (the Graph API endpoint), and uses a credential (the OAuth Profile) to authenticate as an identity (the Service Principal).


4. Per-Artifact Classification

Business Rule

Current classification: autonomous_identity with identitySubtype: "business_rule" Correct classification: automation with automationSubtype: "business_rule"

Justification: A Business Rule is a JavaScript snippet stored in sys_script that fires when a database event occurs on a specified table. It does not authenticate anywhere. It does not hold role assignments. It does not appear as an actor in audit logs. It is pure configuration: "when this event happens, run this code."

The Business Rule's execution authority comes entirely from delegation. It runs in one of three contexts:

  • System context: The ServiceNow platform executes it with full system privileges (no specific user session)
  • Current user context: It executes in the session of the user whose action triggered the event
  • Configured user context: A specific user is configured via run_as (less common for BRs)

In all three cases, the Business Rule borrows authority from an identity. It IS NOT that identity.

Properties unique to this subtype:

  • triggerTable — which table this BR fires on
  • triggerCondition — the condition filter (e.g., current.category == 'software')
  • triggerTiming — before/after/async/display
  • triggerOperation — insert/update/delete/query
  • executionContext — system/current_user/configured_user
  • scriptContent — the JavaScript (or reference to Script Include)
  • isActive — enabled/disabled state

Script Include

Current classification: autonomous_identity with identitySubtype: "script_include" (implied) Correct classification: automation with automationSubtype: "script_include"

Justification: A Script Include is a reusable JavaScript class or function stored in sys_script_include. It is invoked by other automation artifacts (Business Rules, Scheduled Jobs, other Script Includes, Flow Designer Actions). It does not fire on its own. It does not authenticate. It does not hold roles. It is a code library.

The Script Include is the most obviously non-identity artifact in the current model. It is never autonomous — it only executes when called by another artifact. It has no trigger, no schedule, no event binding. It is a function definition, not an actor.

Critical insight: Script Includes are where the actual outbound HTTP calls live in the ServiceNow-Entra execution chain. The AzureGraphRouter Script Include contains the code that instantiates RESTMessageV2, sets the endpoint, and calls execute(). This makes the Script Include the code-level intermediary between the triggering Business Rule and the outbound connection (REST Message).

Properties unique to this subtype:

  • className — the JavaScript class name
  • apiName — the Script Include's API name (for scripted REST APIs)
  • isClientCallable — whether it can be invoked from the client side
  • calledBy — which automations invoke this SI (derived from CALLS edges)
  • outboundConnections — which REST Messages/HTTP endpoints this SI calls

REST Message (Template)

Current classification: Not explicitly classified in the current model (sometimes folded into autonomous_identity, sometimes omitted) Correct classification: connection with connectionSubtype: "rest_message"

Justification: A REST Message template (sys_rest_message) defines an outbound HTTP integration. It specifies: target host (e.g., https://graph.microsoft.com), authentication profile (e.g., the OAuth Profile), and default headers. It is a connection configuration — it defines WHERE to connect and HOW to authenticate.

A REST Message template does not execute anything on its own. It does not authenticate — it references an OAuth Profile that provides credentials. It does not hold roles. It is infrastructure configuration.

Properties unique to this subtype:

  • targetHost — the base URL of the external system
  • authenticationProfile — reference to the OAuth Profile or credential
  • httpMethods — which HTTP methods are defined (GET, POST, etc.)
  • contentType — the default content type
  • isActive — enabled/disabled state

REST Message (Endpoint/Method)

Current classification: Not explicitly modeled Correct classification: connection with connectionSubtype: "rest_message_function"

Justification: A REST Message HTTP Method (sys_rest_message_fn) defines a specific operation on a REST Message template — e.g., "GET /users/{id}" or "POST /teams/{id}/channels". It inherits the base URL and authentication from the parent REST Message template.

This is a more specific connection configuration. It defines not just WHERE to connect but WHAT to do at that endpoint.

Properties unique to this subtype:

  • httpMethod — GET, POST, PUT, DELETE, PATCH
  • relativePath — the endpoint path (e.g., /v1.0/users/{id})
  • parentRestMessage — reference to the parent REST Message template
  • queryParameters — configured query string parameters

OAuth Provider (Config)

Current classification: Not explicitly modeled Correct classification: credential with credentialSubtype: "oauth_provider"

Justification: An OAuth Provider (oauth_entity) in ServiceNow defines an OAuth 2.0 configuration: the authorization server, token endpoint, and client credentials. It is the mechanism by which ServiceNow obtains access tokens for outbound API calls.

This is authentication infrastructure. It stores the client_id, references the client_secret, defines the token endpoint, and specifies the grant type. It is USED BY connections (REST Messages) to authenticate outbound calls. It is not an identity — it is the material that enables an identity to authenticate.

Properties unique to this subtype:

  • clientId — the OAuth client identifier
  • tokenEndpoint — where to request tokens
  • grantType — client_credentials, authorization_code, etc.
  • scopes — requested OAuth scopes
  • isActive — enabled/disabled state

OAuth Profile

Current classification: Not explicitly modeled (or sometimes conflated with the OAuth entity) Correct classification: credential with credentialSubtype: "oauth_profile"

Justification: An OAuth Profile (oauth_entity_profile) is the binding between a specific REST Message and a specific OAuth Provider. It defines: "when this REST Message needs authentication, use this OAuth Provider's credentials to obtain a token."

This is a credential binding configuration. It is not an identity. It is not even the credential itself — it is the association between a connection (REST Message) and a credential (OAuth Provider).

Design decision: Whether to model the OAuth Profile as a separate node or as a property/edge depends on granularity needs. For the MVP, modeling it as a property on the AUTHENTICATES_VIA edge between the connection and the credential is cleaner than creating a separate node for what is essentially a join record.

For the corrected model, the OAuth Provider (with credentialSubtype: "oauth_provider") absorbs the relevant properties of the OAuth Profile. The connection (REST Message) has an AUTHENTICATES_VIA edge to the credential (OAuth Provider).

Service Principal (Entra)

Current classification: autonomous_identity with identitySubtype: "service_principal" Correct classification: identity with identitySubtype: "service_principal"

Justification: A Service Principal is the canonical example of a non-human identity. It authenticates via OAuth (client_credentials, certificates, or federated identity), holds Entra role assignments (e.g., Application.ReadWrite.All), and appears as the actor in Azure sign-in logs.

This is one of only a few artifacts that correctly belong in the identity type. The Service Principal satisfies all three criteria of the identity test: it authenticates, it holds authorization grants, and it appears as an actor.

Properties (unchanged from current model):

  • identitySubtypeservice_principal
  • appId — the application/client ID
  • servicePrincipalType — Application, ManagedIdentity, etc.
  • signInAudience — AzureADMyOrg, AzureADMultipleOrgs, etc.

Application Registration (Entra)

Current classification: Sometimes modeled as autonomous_identity, sometimes omitted Correct classification: identity with identitySubtype: "app_registration"

Justification: An Entra Application Registration is the "application object" that defines the app's identity, credentials, and requested permissions. When an Application Registration is deployed to a tenant, it creates a Service Principal (the "service principal object"). Both are identity entities.

In some modeling approaches, the Application Registration is the canonical identity and the Service Principal is its per-tenant instantiation. For SecurityV0's purposes, both should be modeled as identities. The Application Registration holds the credential configuration (certificates, secrets), while the Service Principal holds the tenant-specific role assignments.

Important nuance: The AUTHENTICATES_TO edge connects the Application Registration (or Service Principal) in Entra to the integration user in ServiceNow. This is the cross-system identity binding.

Role Assignment

Current classification: role Correct classification: role (unchanged)

Justification: Role assignments are correctly modeled as role entities. The itil role, hr_admin role, and Application.ReadWrite.All scope are all groupings of permissions assigned to identities.

Permission / Scope

Current classification: permission Correct classification: permission (unchanged)

Justification: Individual permissions and scopes are correctly modeled as permission entities. The incident.write ACL and User.Read.All scope are atomic capability grants.

Incident Table (Trigger)

Current classification: resource Correct classification: resource (unchanged)

Justification: The incident table is a resource. It is the trigger source for Business Rules (via TRIGGERS_ON) and the target of read/write operations (via APPLIES_TO from permissions). It is correctly modeled as a resource.

Note: The same resource can serve multiple roles in the execution chain:

  • As a trigger source: BusinessRule -[TRIGGERS_ON]-> incident table
  • As a data target: Permission -[APPLIES_TO]-> incident table
  • As an execution target: Identity -[EXECUTES_ON]-> incident table

These are different relationship types to the same resource, not different entity types.

Graph API Endpoint (Target)

Current classification: resource (sometimes omitted) Correct classification: resource with resourceSubtype: "api_endpoint"

Justification: The Microsoft Graph API endpoint (https://graph.microsoft.com/v1.0/users) is the external resource being acted upon. It is the target of outbound API calls.

Important distinction: The Graph API endpoint is a resource (the thing being accessed). The REST Message template that defines how to reach it is a connection (the configuration for reaching it). These are different concepts.

Connection: REST Message "Graph - sn-ticket-router" (HOW to reach the target)
Resource: Graph API /v1.0/users (WHAT is being accessed)

5. Edge Type Implications

Edges That Change

The reclassification of entities requires significant changes to edge semantics. Here is a complete analysis of every affected edge type.

AUTHENTICATES_TO

Current semantics: Identity -> Identity (cross-system authentication binding) New semantics: Identity -> Identity (unchanged — but the set of "identity" nodes shrinks)

What changes: AUTHENTICATES_TO currently connects a Business Rule (classified as identity) to a Service Principal (also identity). After reclassification, the Business Rule is an automation, so AUTHENTICATES_TO no longer applies to it. Instead, the chain is:

BEFORE (wrong):
BusinessRule [identity] -[AUTHENTICATES_TO]-> ServicePrincipal [identity]

AFTER (correct):
BusinessRule [automation] -[RUNS_AS]-> SystemIdentity [identity] -[AUTHENTICATES_TO]-> ServicePrincipal [identity]

Or more precisely, the full chain:

  BusinessRule [automation] -[CALLS]-> ScriptInclude [automation]
ScriptInclude [automation] -[USES_CONNECTION]-> RESTMessage [connection]
RESTMessage [connection] -[AUTHENTICATES_VIA]-> OAuthProvider [credential]
OAuthProvider [credential] -[AUTHENTICATES_AS]-> ServicePrincipal [identity]

The AUTHENTICATES_TO edge remains valid but now only connects true identities. In the ServiceNow-Entra case, the ServiceNow integration user (identity) AUTHENTICATES_TO the Entra Service Principal (identity) via the OAuth credential.

RUNS_AS

Current semantics: Identity -> Identity (automation borrows another identity's authority) New semantics: Automation -> Identity (automation delegates to an identity for execution)

What changes: The source node type changes from identity to automation. This is more correct — it explicitly states that the thing doing the delegation is an automation, not an identity.

BEFORE: BusinessRule [identity] -[RUNS_AS]-> SystemContext [identity]
AFTER: BusinessRule [automation] -[RUNS_AS]-> SystemContext [identity]

TRIGGERS_ON

Current semantics: Identity -> Resource (automation is triggered by this resource/event) New semantics: Automation -> Resource (automation is triggered by this resource/event)

What changes: The source node type changes from identity to automation. Semantically identical but type-correct.

CALLS

Current semantics: Not yet in the platform type system (proposed in Round 2) New semantics: Automation -> Automation (one automation invokes another)

What changes: CALLS connects two automation nodes. BusinessRule -[CALLS]-> ScriptInclude. This is now type-homogeneous (automation -> automation), which is cleaner than the current proposal of identity -> identity.

OWNED_BY

Current semantics: Identity -> Owner New semantics: (Identity | Automation | Connection | Credential) -> Owner

What changes: Ownership now applies to all entity types, not just identities. A Business Rule has a creator/owner. An OAuth Profile has a maintainer. A REST Message has a team responsible for it. This is semantically correct — ownership is a cross-cutting concern, not specific to identities.

EXECUTES_ON

Current semantics: Identity -> Resource (identity has actually executed actions on this resource) New semantics: Identity -> Resource (unchanged — only true identities execute)

What changes: Execution evidence is only attached to identities, not automations. This is correct: the syslog_transaction records the identity (user context), not the Business Rule. The flow context records the flow definition, but the actual execution actor is the run_as identity.

However, we also need a new edge for automations:

New Edge: USES_CONNECTION

Semantics: Automation -> Connection (this automation uses this connection for outbound calls)

The Script Include calls new RESTMessageV2("Graph - sn-ticket-router", "POST"). This is the automation using a connection. It is not "executing on" the REST Message (that would be the identity's execution). It is "using" the connection configuration to make outbound calls.

ScriptInclude [automation] -[USES_CONNECTION]-> RESTMessage [connection]

New Edge: AUTHENTICATES_VIA (Repurposed)

Current semantics: Identity -> Credential New semantics: (Identity | Connection) -> Credential

Connections also authenticate via credentials. The REST Message template uses the OAuth Profile for authentication. This is the same concept as an identity using a credential, just applied to a connection.

RESTMessage [connection] -[AUTHENTICATES_VIA]-> OAuthProvider [credential]

New Edge: AUTHENTICATES_AS

Semantics: Credential -> Identity (this credential authenticates as this identity)

An OAuth Provider with a specific client_id authenticates as a specific Service Principal. This is the binding between a credential and the identity it represents.

OAuthProvider [credential] -[AUTHENTICATES_AS]-> ServicePrincipal [identity]

This replaces the current conflated AUTHENTICATES_TO edge that goes from a Business Rule to a Service Principal. The real chain is: automation -> uses connection -> connection uses credential -> credential authenticates as identity.

Complete Edge Type Reference

Edge TypeFrom TypeTo TypeMeaning
OWNED_BYanyhuman_identity (-> Owner)Accountability relationship
BELONGS_TOhuman_identityhuman_identityOwnership hierarchy
HAS_ROLEidentityroleIdentity holds this role
GRANTSrolepermissionRole includes this permission
APPLIES_TOpermissionresourcePermission scopes to this resource
AUTHENTICATES_TOidentityidentityCross-system identity binding
AUTHENTICATES_VIAidentity / connectioncredentialUses this credential for auth
AUTHENTICATES_AScredentialidentityCredential represents this identity
EXECUTES_ONidentityresourceProof of actual execution
RUNS_ASautomationidentityAutomation delegates to this identity
TRIGGERS_ONautomationresourceAutomation triggered by this resource/event
CALLSautomationautomationAutomation invokes another automation
USES_CONNECTIONautomationconnectionAutomation uses this outbound connection
CREATED_BYanyhuman_identityWho created this entity
DELEGATES_TOidentityidentityIdentity delegates authority
APPROVED_BYanyhuman_identityWho approved this entity
MEMBER_OFidentity / human_identityroleGroup/team membership

6. The RUNS_AS vs IS Distinction

The Core Problem

A Business Rule RUNS AS a system context, but it IS NOT that system context. A Scheduled Job RUNS AS a configured user, but it IS NOT that user. A Flow Designer Flow RUNS AS a specified identity, but it IS NOT that identity.

The current model conflates "runs as" with "is" by making all of these identity nodes. The corrected model separates them:

CONFLATED (current):
BusinessRule [identity] -[HAS_ROLE]-> Role
(implies: the BR itself holds this role)

CORRECT (proposed):
BusinessRule [automation] -[RUNS_AS]-> SystemContext [identity] -[HAS_ROLE]-> Role
(states: the BR delegates to the SystemContext, which holds this role)

Why This Matters

1. Different lifecycle. An automation can be modified, disabled, or deleted without affecting the identity it runs as. Conversely, the identity's roles can change without the automation's configuration changing. These are independent lifecycle entities that should be tracked independently.

2. Fan-out. Multiple automations may RUNS_AS the same identity. If 50 Business Rules all run as the "system" context, the current model creates 50 identity nodes that are all conceptually the same system identity. The corrected model creates 50 automation nodes that all point to a single system identity node via RUNS_AS.

3. Authority analysis. "How many automations use admin-level execution?" requires counting automations that RUNS_AS an identity with admin roles. In the corrected model, this is a graph traversal: automation -[RUNS_AS]-> identity -[HAS_ROLE]-> role WHERE role.is_privileged = true. In the current model, you would need to check each "identity" node's identitySubtype to filter out actual identities from automations, then check roles — a fragile and error-prone query.

4. Ownership clarity. The Business Rule's owner is the person who created or maintains the automation configuration. The Service Principal's owner is the person accountable for the identity's credentials and access. These are different people with different accountability. The current model produces findings like "this identity has orphaned ownership" without clarifying WHICH thing has orphaned ownership — the automation config or the underlying identity.

The RUNS_AS Relationship Model

┌──────────────────────┐     RUNS_AS      ┌──────────────────────┐
│ automation │─────────────────▶│ identity │
│ │ │ │
│ Business Rule │ run_as_type: │ System Context │
│ "Auto-route tickets" │ "system" │ (sys_user: system) │
│ │ │ │
│ Has: trigger config │ │ Has: HAS_ROLE edges │
│ Has: script content │ │ Has: credentials │
│ Has: enable/disable │ │ Has: EXECUTES_ON │
│ Has: OWNED_BY edges │ │ Has: OWNED_BY edges │
└──────────────────────┘ └──────────────────────┘
│ │
│ TRIGGERS_ON │ HAS_ROLE
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ resource │ │ role │
│ incident table │ │ admin / itil / etc │
└──────────────────────┘ └──────────────────────┘

The automation has its own lifecycle (created, modified, enabled, disabled, deleted) and its own ownership chain. The identity has its own lifecycle and ownership chain. They are connected by RUNS_AS but are fundamentally independent entities.

RUNS_AS Properties

PropertyTypeDescription
runAsTypeenumsystem / configured_user / current_user / inherited
configuredUserIdstring?If configured_user, which user is configured
effectivePrivilegeLevelstring?admin / standard / restricted
sincedatetime?When this RUNS_AS binding was established

The Three RUNS_AS Patterns

Pattern 1: System Context

BusinessRule -[RUNS_AS {runAsType: "system"}]-> SystemIdentity

The automation runs with full system privileges. No specific user session. This is the most privileged and most common pattern for server-side Business Rules.

Pattern 2: Configured User

ScheduledJob -[RUNS_AS {runAsType: "configured_user", configuredUserId: "sn-integration-user"}]-> IntegrationUser

The automation runs as a specific, configured user account. The automation explicitly borrows that user's roles and session context.

Pattern 3: Current User (Inherited)

BusinessRule -[RUNS_AS {runAsType: "current_user"}]-> (dynamic, depends on triggering session)

The automation runs in the context of whoever triggered the event. If a human user inserts an incident, the BR runs as that human. If another automation triggers the insert, the BR runs as that automation's RUNS_AS identity. This is the least privileged pattern but also the hardest to analyze statically.

For Pattern 3, the platform should model this as a RUNS_AS edge with runAsType: "current_user" and no configuredUserId. The path materializer should note that the effective authority is dynamic and cannot be statically determined. This is a limitation that should be transparently communicated in findings.


7. NormalizedNodeType Enum Proposal

The Proposed Enum

export type NormalizedNodeType =
| "identity" // Something that authenticates and acts
| "automation" // Configuration that defines execution logic
| "connection" // Outbound target configuration
| "credential" // Authentication material
| "role" // Grouping of permissions
| "permission" // Individual capability
| "resource" // Thing being acted upon
| "execution_evidence" // Proof of execution
| "human_identity"; // Human user account (for ownership derivation)

Justification for Each Type

identity (renamed from autonomous_identity)

Renamed to drop the "autonomous" qualifier. Not all identities are autonomous — a machine account that is only used by human-triggered processes is still an identity. The "autonomous" characteristic is a property (executionMode), not a type.

Subtypes: service_principal, oauth_app, github_app, machine_account, managed_identity, bot, agent

automation (NEW)

Configuration artifacts that define execution logic. They do not authenticate, do not hold their own roles, and are not actors. They delegate to identities via RUNS_AS.

Subtypes: business_rule, script_include, flow_designer_flow, scheduled_job, system_execution, workflow, script_action

connection (NEW)

Outbound integration configurations. They define WHERE to connect and HOW to authenticate, but they are not identities and they are not resources.

Subtypes: rest_message, rest_message_function, soap_message, http_connection, jdbc_connection, ldap_connection, email_connection

credential (unchanged)

Authentication material. Already correctly modeled.

Subtypes: oauth_provider, oauth_profile, oauth_client_secret, certificate, pat, api_key, oidc_token, ssh_key

Note: oauth_provider and oauth_profile are added as subtypes. The current model has credential but does not include ServiceNow's OAuth entities as credential nodes — they are sometimes conflated with identity nodes.

role (unchanged)

Groupings of permissions. Already correctly modeled.

permission (unchanged)

Individual capabilities. Already correctly modeled.

resource (unchanged)

Things being acted upon. Already correctly modeled.

execution_evidence (unchanged)

Proof of execution. Already correctly modeled.

human_identity (unchanged)

Human user accounts for ownership derivation. Already correctly modeled.

Subtype Interfaces

// --- Identity subtypes ---
interface IdentityProperties {
identitySubtype:
| "service_principal"
| "oauth_app"
| "github_app"
| "machine_account"
| "managed_identity"
| "bot"
| "agent"
| "pat_identity"; // For PAT-based identities (GitHub, etc.)
executionMode: "autonomous" | "operator_assisted" | "human_triggered" | "unknown";
securityRelevance: "active_external" | "dormant_authority" | "internal_inventory";
lastActivityAt?: string;
ownershipState: OwnershipState;
}

// --- Automation subtypes ---
interface AutomationProperties {
automationSubtype:
| "business_rule"
| "script_include"
| "flow_designer_flow"
| "scheduled_job"
| "system_execution"
| "workflow"
| "script_action"
| "fix_script";
executionMode: "autonomous" | "event_driven" | "scheduled" | "invoked" | "unknown";
securityRelevance: "active_external" | "dormant_authority" | "internal_inventory";
triggerType?: "schedule" | "event" | "manual" | "api_call" | "invocation";
triggerTable?: string;
triggerCondition?: string;
isActive: boolean;
lastExecutionAt?: string;
executionCount30d?: number;
scriptContent?: string; // For static analysis
localMutations?: Array<{
table: string;
operation: string;
fields: string[];
}>;
workflowSuppression?: boolean; // setWorkflow(false) detection
}

// --- Connection subtypes ---
interface ConnectionProperties {
connectionSubtype:
| "rest_message"
| "rest_message_function"
| "soap_message"
| "http_connection"
| "jdbc_connection"
| "ldap_connection"
| "email_connection";
targetHost: string;
httpMethod?: string; // For REST message functions
relativePath?: string; // For REST message functions
authenticationMethod?: "oauth2" | "basic" | "api_key" | "certificate" | "none";
isActive: boolean;
egressCategory?: "external" | "internal" | "infrastructure";
}

// --- Credential subtypes (extended) ---
interface CredentialProperties {
credentialSubtype:
| "oauth_provider"
| "oauth_profile"
| "oauth_client_secret"
| "certificate"
| "pat"
| "api_key"
| "oidc_token"
| "ssh_key"
| "federation_trust"
| "role_assumption";
expiresAt?: string;
lastUsedAt?: string;
rotatedAt?: string;
clientId?: string; // For OAuth credentials
tokenEndpoint?: string; // For OAuth credentials
grantType?: string; // For OAuth credentials
scopes?: string[]; // For OAuth credentials
issuingSystem?: string;
targetSystem?: string;
}

8. Corrected Graph Structure

The AzureGraphRouter Execution Chain

This is the primary execution chain from the Entra-ServiceNow connector. Let me show it with the corrected types.

Current Model (Wrong)

[identity: business_rule] BR: "Auto-route identity tickets"
-[TRIGGERS_ON]-> [resource] incident table
-[RUNS_AS]-> [identity: service_principal] SP: sn-ticket-router
-[EXECUTES_ON]-> [resource?] REST Message: "Graph - sn-ticket-router"
-[AUTHENTICATES_TO]-> [identity: service_principal] SP: sn-ticket-router (Entra)

[identity: business_rule] BR: "Auto-route identity tickets (2)"
-[TRIGGERS_ON]-> [resource] incident table
-[RUNS_AS]-> [identity: service_principal] SP: sn-ticket-router
-[EXECUTES_ON]-> [resource?] REST Message: "Graph - sn-ticket-router"

[identity: script_include?] SI: AzureGraphRouter
-[RUNS_AS]-> [identity: service_principal] SP: sn-ticket-router
-[EXECUTES_ON]-> [resource?] REST Message: "Graph - sn-ticket-router"

Problems:

  1. Business Rules are labeled as "identity"
  2. The Script Include is labeled as "identity"
  3. The REST Message is sometimes a resource, sometimes omitted
  4. The OAuth Profile and Provider are missing entirely
  5. AUTHENTICATES_TO goes from a Business Rule to a Service Principal (a BR does not authenticate)
  6. The causal chain (BR -> SI -> REST -> OAuth -> SP) is collapsed into parallel edges
  7. RUNS_AS goes from BR to SP (a BR does not "run as" an SP — it runs in system context, and the SP is reached via an OAuth credential chain)

Corrected Model

[automation: business_rule] BR: "Auto-route identity tickets via Entra"
-[TRIGGERS_ON]-> [resource: table] incident table
-[RUNS_AS {runAsType: "system"}]-> [identity: system_execution] ServiceNow System Context
-[CALLS]-> [automation: script_include] SI: AzureGraphRouter

[automation: business_rule] BR: "Auto-route identity tickets via Entra (2)"
-[TRIGGERS_ON]-> [resource: table] incident table
-[RUNS_AS {runAsType: "system"}]-> [identity: system_execution] ServiceNow System Context
-[CALLS]-> [automation: script_include] SI: AzureGraphRouter

[automation: script_include] SI: AzureGraphRouter
-[USES_CONNECTION]-> [connection: rest_message] REST: "Graph - sn-ticket-router"

[connection: rest_message] REST: "Graph - sn-ticket-router"
-[AUTHENTICATES_VIA]-> [credential: oauth_provider] OAuth: "Azure Graph OAuth Client"
-[TARGETS]-> [resource: api_endpoint] Microsoft Graph API /v1.0/users

[credential: oauth_provider] OAuth: "Azure Graph OAuth Client"
-[AUTHENTICATES_AS]-> [identity: service_principal] SP: sn-ticket-router (Entra)

[identity: service_principal] SP: sn-ticket-router (Entra)
-[HAS_ROLE]-> [role] Application.ReadWrite.All
-[HAS_ROLE]-> [role] User.Read.All

[role] Application.ReadWrite.All
-[GRANTS]-> [permission] application.readwrite.all
-[GRANTS]-> [permission] user.read.all

[permission] application.readwrite.all
-[APPLIES_TO]-> [resource: api_endpoint] Microsoft Graph API /applications

[permission] user.read.all
-[APPLIES_TO]-> [resource: api_endpoint] Microsoft Graph API /users

Visual Representation

                          TRIGGER LAYER
┌─────────────────────┐
│ [resource: table] │
│ incident table │
└──────────┬──────────┘
│ TRIGGERS_ON

AUTOMATION LAYER
┌────────────────────┼────────────────────┐
│ │ │
┌─────────▼──────────┐ ┌──────▼───────────┐ ┌──────▼───────────┐
│[automation: BR] │ │[automation: BR] │ │ │
│"Auto-route (1)" │ │"Auto-route (2)" │ │ │
└─────────┬──────────┘ └──────┬───────────┘ │ │
│ CALLS │ CALLS │ │
└────────┬───────────┘ │ │
│ │ RUNS_AS │
▼ ▼ (from BRs) │
┌────────────────────┐ ┌────────────────────┐ │
│[automation: SI] │ │[identity] │ │
│ AzureGraphRouter │ │ SN System Context │ │
└─────────┬──────────┘ └────────────────────┘ │
│ USES_CONNECTION │
│ │
CONNECTION LAYER │
▼ │
┌────────────────────┐ │
│[connection] │ │
│ REST: Graph - │ │
│ sn-ticket-router │ │
└──┬──────────┬──────┘ │
│ │ │
AUTHENTICATES_VIA TARGETS │
│ │ │
▼ ▼ │
┌──────────────┐ ┌────────────────────┐ │
│[credential] │ │[resource: endpoint]│ │
│ OAuth: Azure │ │ Graph API /users │ │
│ Graph Client │ └────────────────────┘ │
└──────┬───────┘ │
│ AUTHENTICATES_AS │
│ │
│ IDENTITY LAYER │
▼ │
┌────────────────────┐ │
│[identity: SP] │ │
│ sn-ticket-router │ │
│ (Entra) │ │
└──────┬─────────────┘ │
│ HAS_ROLE │
│ │
AUTHORIZATION LAYER │
▼ │
┌────────────────────┐ │
│[role] │ │
│Application.RW.All │ │
└──────┬─────────────┘ │
│ GRANTS │
▼ │
┌────────────────────┐ │
│[permission] │ │
│application.rw.all │ │
└──────┬─────────────┘ │
│ APPLIES_TO │
▼ │
┌────────────────────┐
│[resource: endpoint]│
│ Graph API /apps │
└────────────────────┘

What This Visualization Shows

The graph now has clear layers:

  1. Trigger Layer — Resources that fire events (incident table)
  2. Automation Layer — Configuration artifacts that respond to triggers (BRs, SIs)
  3. Connection Layer — Outbound integration configurations (REST Messages)
  4. Credential Layer — Authentication material (OAuth Profiles)
  5. Identity Layer — Entities that authenticate and hold authorization (SPs)
  6. Authorization Layer — Roles, permissions, and target resources

Each layer communicates with the next through a specific edge type:

  • Trigger -> Automation: TRIGGERS_ON
  • Automation -> Automation: CALLS
  • Automation -> Connection: USES_CONNECTION
  • Connection -> Credential: AUTHENTICATES_VIA
  • Credential -> Identity: AUTHENTICATES_AS
  • Identity -> Role: HAS_ROLE
  • Role -> Permission: GRANTS
  • Permission -> Resource: APPLIES_TO

This layered structure is not just visually cleaner — it enables precise queries:

  • "Which automations use this credential?" — follow USES_CONNECTION -> AUTHENTICATES_VIA backward
  • "Which identity does this automation ultimately act as?" — follow RUNS_AS or USES_CONNECTION -> AUTHENTICATES_VIA -> AUTHENTICATES_AS
  • "Which automations are affected if this SP's credentials expire?" — follow AUTHENTICATES_AS backward -> AUTHENTICATES_VIA backward -> USES_CONNECTION backward

9. Impact on Existing Architecture

Storage Layer

The entities collection already uses a single-collection polymorphic design with entity_type as the discriminator. Adding automation and connection types requires:

  1. Update the entity_type enum validation in the storage adapter
  2. Add new indexes for the new types:
    db.entities.createIndex({ tenant_id: 1, entity_type: 1, "properties.automationSubtype": 1 });
    db.entities.createIndex({ tenant_id: 1, entity_type: 1, "properties.connectionSubtype": 1 });
  3. Update the baseline_metadata.entity_counts schema to include automations and connections counts

Ingestion Pipeline

The ingestion pipeline at sv0-platform/src/ingestion/ validates incoming NormalizedGraph against Zod schemas. The schema must be updated to accept the new node types and their property interfaces.

API Layer

The entity listing API at sv0-platform/src/api/routes/ filters by entity_type. New filter values (automation, connection) must be added. The blast radius API should be updated to only count identity and automation nodes in threat surface calculations, excluding connection nodes.

UI Layer

The UI's entity list, graph explorer, and entity detail views all use entity_type for display logic. The changes are:

  • EntityBadge.tsx — add colors and labels for automation and connection
  • GraphExplorer — add layer assignment for automation and connection nodes
  • EntityDetail — add subtype-specific detail tabs for automation and connection entities
  • Filters — add automation/connection as filter options

10. Impact on Path Materialization

Current Path Materializer

The path materializer currently builds execution paths starting from entity_type === "identity" nodes. It traverses:

identity -[HAS_ROLE]-> role -[GRANTS]-> permission -[APPLIES_TO]-> resource

And for cross-system paths:

identity -[AUTHENTICATES_TO]-> identity -[HAS_ROLE]-> ...

And for automation paths:

identity(automation) -[RUNS_AS]-> identity(SP) -[HAS_ROLE]-> ...

Updated Path Materializer

After reclassification, path materialization must start from BOTH identity and automation nodes:

For identity nodes:

identity -[HAS_ROLE]-> role -[GRANTS]-> permission -[APPLIES_TO]-> resource
identity -[AUTHENTICATES_TO]-> identity -[HAS_ROLE]-> ...

(Unchanged.)

For automation nodes:

automation -[RUNS_AS]-> identity -[HAS_ROLE]-> role -[GRANTS]-> permission -[APPLIES_TO]-> resource

The path materializer follows RUNS_AS to reach the identity, then continues as before. This is already the semantic behavior — the change is that the starting node type is now automation instead of identity with identitySubtype: "business_rule".

For the full execution chain:

automation(BR) -[CALLS]-> automation(SI) -[USES_CONNECTION]-> connection(REST) -[AUTHENTICATES_VIA]-> credential(OAuth) -[AUTHENTICATES_AS]-> identity(SP) -[HAS_ROLE]-> role -[GRANTS]-> permission -[APPLIES_TO]-> resource

The path materializer should be able to traverse this full chain, producing an execution path that shows every layer from trigger to target resource.

Path Start Nodes

Currently, execution_paths are only materialized on identity nodes. After reclassification, they should be materialized on automation nodes as well. An automation's execution paths are the union of all paths reachable from the identities it delegates to (via RUNS_AS) and the resources it connects to (via USES_CONNECTION -> ... -> identity -> HAS_ROLE -> ...).

This means the execution_paths array should appear on:

  • identity nodes (direct: HAS_ROLE -> GRANTS -> APPLIES_TO)
  • automation nodes (delegated: RUNS_AS -> identity's paths, plus USES_CONNECTION -> ... -> identity's paths)

11. Impact on Findings and Evaluator

Finding Type Changes

orphaned_ownership:

  • Currently triggers on identity nodes. Should now trigger on BOTH identity and automation nodes.
  • An automation with orphaned ownership is a different finding than an identity with orphaned ownership:
    • orphaned_automation — "This automation configuration has no active maintainer. The Business Rule 'Auto-route tickets' was created by a departed employee and has no team ownership."
    • orphaned_identity — "This service principal has no active owner. The SP 'sn-ticket-router' was owned by IT-Automation-Team which was disbanded."
  • These are two different risk profiles. The automation finding indicates configuration maintenance risk. The identity finding indicates credential and access management risk.

dormant_authority:

  • Should primarily trigger on identity nodes. An identity with broad permissions but no recent execution is a genuine risk.
  • For automation nodes, the equivalent is "dormant_automation" — an automation that is configured but has not executed. This is a lower risk (no credential exposure) but indicates configuration debt.

scope_drift:

  • Only applies to identity nodes. Automations do not have roles that drift. The identity they delegate to may experience scope drift, but that is a finding on the identity, not the automation.

New Finding Types Enabled

The type separation enables findings that were previously impossible:

broken_execution_chain:

  • "This automation delegates to an identity whose credentials have expired."
  • Requires following: automation -[RUNS_AS]-> identity, then checking the identity's credential state.
  • Currently impossible because the automation and identity are the same node.

orphaned_automation_active_identity:

  • "This automation has no owner, but the identity it delegates to is well-managed."
  • The opposite is also interesting: "This identity is orphaned, but 12 automations still delegate to it."

connection_without_credential:

  • "This REST Message has no valid OAuth Profile configured."
  • Only possible when connections are separate from identities and credentials.

credential_shared_across_chains:

  • "This OAuth credential is used by 5 different automation chains."
  • Only possible when credentials are modeled as separate nodes that multiple connections reference.

12. Impact on OAA Export

Round 4 Conclusion Holds

Round 4 concluded that OAA is an export format, not the internal data model. This reclassification reinforces that conclusion.

OAA Mapping with Corrected Types

SecurityV0 TypeOAA Mapping
identitylocal_user within source system Application
automationresource with resource_type: "automation"
connectionresource with resource_type: "connection"
credentialCustom property on local_user (no OAA credential entity)
rolerole
permissionpermission
resourceresource
execution_evidenceNot exported (no OAA equivalent)
human_identityIdP identity link

The corrected types actually make OAA export cleaner:

  • Identities map naturally to local_user
  • Automations and connections map naturally to resource subtypes
  • The current conflation of putting Business Rules as local_user in OAA would be semantically wrong — they are not "users" of the application

13. Counter-Arguments and Rebuttals

"Rounds 1-4 unanimously agreed to keep entity_type: identity"

That was a cost-based decision, not a correctness-based one. Round 2's architect explicitly stated: "entity_type is a storage-level discriminator, identitySubtype is semantic. Use each at the right layer." This was pragmatic advice for a migration-constrained environment. The founder has removed the constraint.

"The path materializer will need major changes"

Yes, but those changes make it more correct. Currently the path materializer contains implicit branching: "if this identity node has identitySubtype in [business_rule, scheduled_job], follow RUNS_AS instead of HAS_ROLE." This is a code smell — the type system should express the difference, not conditional logic buried in the traversal algorithm.

After reclassification, the path materializer has clean, type-based branching:

  • Start node is identity -> follow HAS_ROLE
  • Start node is automation -> follow RUNS_AS to reach identity, then follow HAS_ROLE

"This adds complexity to the type system"

It adds two types (automation, connection) and removes one overloaded type (autonomous_identity which was doing triple duty). The net complexity is REDUCED because each type now has a single, clear responsibility.

"We can achieve the same result with identitySubtype"

Only at the UI and query layers. At the schema level, an entity with entity_type: "identity" and identitySubtype: "business_rule" carries a false semantic claim: it claims to be an identity. Every piece of code that processes identity nodes must check "but is it really an identity, or is it an automation pretending to be one?" This is the definition of a leaky abstraction.

"Flows and Scheduled Jobs are close to identities — they have PARTIAL actor status"

Partial is not sufficient. The identity test requires at least 2 of 3 criteria. Flows and Scheduled Jobs score 1/3 (PARTIAL on authorization, PARTIAL on logging). They are automations that delegate to identities, not identities themselves.

If we start admitting "close enough" entities as identities, we degrade the type system. Next, REST Messages become identities because they "kinda authenticate" (they reference credentials). Then OAuth Profiles become identities because they "kinda have scopes." The line must be drawn at the identity test, not at convenience.

"This will break the connector's transformer.py"

Yes. The transformer currently emits all automation nodes as nodeType: "autonomous_identity". After reclassification, it emits them as nodeType: "automation". This is a mechanical change — updating the nodeType field in the transformer's node construction functions. The connector's EdgeResolver and correlator are unaffected because they work with source system entities, not NormalizedGraph types.


14. Migration Path

The founder has stated there are no active clients and the model can be rewritten from scratch. This makes migration straightforward:

Phase 1: Type System Update (Platform)

  1. Update NormalizedNodeType in types.ts to the new 9-type enum
  2. Update Zod validation schemas for ingestion
  3. Update storage adapter to accept new types
  4. Update indexes for new type discrimination
  5. Update path materializer for type-based traversal
  6. Update evaluator for type-specific finding logic

Estimated effort: 16-24 hours

Phase 2: Connector Update

  1. Update transformer.py to emit automations as nodeType: "automation"
  2. Update to emit connections as nodeType: "connection"
  3. Update to emit OAuth entities as nodeType: "credential" (subtype: oauth_provider)
  4. Add USES_CONNECTION and AUTHENTICATES_AS edges
  5. Remove AUTHENTICATES_TO edges from automations (keep only between identities)
  6. Update tests

Estimated effort: 16-24 hours

Phase 3: UI Update

  1. Update EntityBadge with automation/connection colors and labels
  2. Update GraphExplorer with new layer assignments
  3. Update EntityDetail with subtype-specific tabs
  4. Update filters for new entity types
  5. Update AutomationFlowDiagram for corrected chain structure

Estimated effort: 12-16 hours

Phase 4: Documentation Update

  1. Update 01-data-model.md with new type hierarchy
  2. Update 05-connectors.md with updated NormalizedNodeType
  3. Update 03-database.md with new indexes and property schemas
  4. Write migration guide (for reference, even though no clients need it)

Estimated effort: 8-12 hours

Total Estimated Effort: 52-76 hours


Appendix A: Full Artifact Classification Table

ArtifactSource SystemCurrent TypeCorrect TypeSubtypeIdentity Test Score
Service PrincipalEntra IDautonomous_identityidentityservice_principal3/3
Application RegistrationEntra IDautonomous_identityidentityapp_registration3/3
OAuth AppEntra IDautonomous_identityidentityoauth_app3/3
Machine Account (sys_user)ServiceNowautonomous_identityidentitymachine_account3/3
GitHub AppGitHubautonomous_identityidentitygithub_app3/3
Managed IdentityAzureautonomous_identityidentitymanaged_identity3/3
Business RuleServiceNowautonomous_identityautomationbusiness_rule0/3
Script IncludeServiceNowautonomous_identityautomationscript_include0/3
Flow Designer FlowServiceNowautonomous_identityautomationflow_designer_flow1/3
Scheduled JobServiceNowautonomous_identityautomationscheduled_job1/3
System ExecutionServiceNowautonomous_identityidentitysystem_execution2/3
Script ActionServiceNow(not modeled)automationscript_action0/3
WorkflowServiceNow(not modeled)automationworkflow1/3
REST Message (template)ServiceNow(not modeled)connectionrest_message0/3
REST Message (function)ServiceNow(not modeled)connectionrest_message_function0/3
SOAP MessageServiceNow(not modeled)connectionsoap_message0/3
OAuth ProviderServiceNow(not modeled)credentialoauth_provider0/3
OAuth ProfileServiceNow(not modeled)credentialoauth_profile0/3
Client SecretEntra IDcredentialcredentialoauth_client_secret0/3
CertificateEntra IDcredentialcredentialcertificate0/3
API KeyVariouscredentialcredentialapi_key0/3
Entra RoleEntra IDroleroledirectoryN/A
ServiceNow RoleServiceNowroleroleapplicationN/A
Graph API ScopeEntra IDpermissionpermissionapi_permissionN/A
ServiceNow ACLServiceNowpermissionpermissionaclN/A
Database TableServiceNowresourceresourcetableN/A
API EndpointVariousresourceresourceapi_endpointN/A
RepositoryGitHubresourceresourcerepositoryN/A

Note on System Execution: The ServiceNow "System" identity scores 2/3 (it holds roles, it appears as actor "system" in logs, but it does not authenticate in the traditional sense). It is modeled as identity with identitySubtype: "system_execution" because it IS the execution authority — Business Rules that run in system context delegate to this identity.


Appendix B: Edge Type Reference

Full Edge Type Enum (Proposed)

export type NormalizedEdgeType =
| "OWNED_BY" // (any) -> human_identity
| "BELONGS_TO" // human_identity -> human_identity (hierarchy)
| "HAS_ROLE" // identity -> role
| "GRANTS" // role -> permission
| "APPLIES_TO" // permission -> resource
| "AUTHENTICATES_TO" // identity -> identity (cross-system)
| "AUTHENTICATES_VIA" // (identity | connection) -> credential
| "AUTHENTICATES_AS" // credential -> identity (NEW)
| "EXECUTES_ON" // identity -> resource (execution evidence)
| "RUNS_AS" // automation -> identity
| "TRIGGERS_ON" // automation -> resource
| "CALLS" // automation -> automation
| "USES_CONNECTION" // automation -> connection (NEW)
| "TARGETS" // connection -> resource (NEW)
| "CREATED_BY" // (any) -> human_identity
| "DELEGATES_TO" // identity -> identity
| "APPROVED_BY" // (any) -> human_identity
| "MEMBER_OF"; // (identity | human_identity) -> role

Edge Validity Matrix

Edge TypeValid Source TypesValid Target Types
OWNED_BYidentity, automation, connection, credentialhuman_identity
BELONGS_TOhuman_identityhuman_identity
HAS_ROLEidentityrole
GRANTSrolepermission
APPLIES_TOpermissionresource
AUTHENTICATES_TOidentityidentity
AUTHENTICATES_VIAidentity, connectioncredential
AUTHENTICATES_AScredentialidentity
EXECUTES_ONidentityresource
RUNS_ASautomationidentity
TRIGGERS_ONautomationresource
CALLSautomationautomation
USES_CONNECTIONautomationconnection
TARGETSconnectionresource
CREATED_BYidentity, automation, connection, credentialhuman_identity
DELEGATES_TOidentityidentity
APPROVED_BYidentity, automationhuman_identity
MEMBER_OFidentity, human_identityrole

This matrix can be enforced at ingestion time — reject edges that violate the source/target type constraints. This is a structural integrity guarantee that the current model cannot provide because everything is an "identity."


Appendix C: Query Pattern Changes

"What can this automation reach?"

Current (wrong):

// Treats automation as identity, directly follows HAS_ROLE
const paths = entity.execution_paths;
// Returns paths from the BR's own (nonexistent) roles... or follows RUNS_AS internally

Corrected:

// Follow RUNS_AS to get the delegated identity, then get that identity's paths
const runsAsEdges = entity.relationships.filter(r => r.type === "RUNS_AS");
for (const edge of runsAsEdges) {
const identity = await db.entities.findOne({ _id: edge.target_id });
paths.push(...identity.execution_paths);
}
// Also follow USES_CONNECTION -> AUTHENTICATES_VIA -> AUTHENTICATES_AS -> identity
const connectionEdges = entity.relationships.filter(r => r.type === "USES_CONNECTION");
// ... traverse to identity, get its paths

"Which automations use this identity?"

Current (impossible without subtype check):

// Would return ALL identities with RUNS_AS edges, including actual identities
const users = db.entities.find({
"relationships.type": "RUNS_AS",
"relationships.target_id": identityId
});
// Must filter by identitySubtype to distinguish automations from identities

Corrected (clean type-based query):

const automations = db.entities.find({
entity_type: "automation",
"relationships.type": "RUNS_AS",
"relationships.target_id": identityId
});
// No subtype filtering needed — entity_type guarantees these are automations

"How many real identities access HR data?"

Current (inflated count):

const count = db.entities.countDocuments({
entity_type: "identity",
"execution_paths.business_domain": "hr"
});
// Returns 47 — includes 3 SPs + 44 BRs/SIs/Flows/Jobs

Corrected (accurate count):

const identityCount = db.entities.countDocuments({
entity_type: "identity",
"execution_paths.business_domain": "hr"
});
// Returns 3 — only real identities

const automationCount = db.entities.countDocuments({
entity_type: "automation",
"execution_paths.business_domain": "hr"
});
// Returns 44 — automation configs that delegate to those 3 identities

"What is the full execution chain from trigger to target?"

Current (requires BFS with subtype heuristics):

// BFS from seed entity, following various edge types
// Must use identitySubtype to determine which edges to follow
// BR -> RUNS_AS -> SP? or BR -> EXECUTES_ON -> REST? Ambiguous.

Corrected (type-driven traversal):

// Start from automation, follow typed edges in order:
// 1. automation -[TRIGGERS_ON]-> resource (trigger)
// 2. automation -[CALLS]-> automation (invocation chain)
// 3. automation -[RUNS_AS]-> identity (delegation)
// 4. automation -[USES_CONNECTION]-> connection (outbound)
// 5. connection -[AUTHENTICATES_VIA]-> credential (auth material)
// 6. credential -[AUTHENTICATES_AS]-> identity (target identity)
// 7. identity -[HAS_ROLE]-> role -[GRANTS]-> permission -[APPLIES_TO]-> resource
//
// Each step follows a specific edge type from a specific node type.
// No heuristics. No subtype checking. Pure type-driven traversal.

Summary of Recommendations

1. Replace the autonomous_identity type with separate identity and automation types.

Identities authenticate, hold authorization, and act. Automations define execution logic and delegate to identities. These are fundamentally different concepts that must not be conflated.

2. Add a connection type for outbound integration configurations.

REST Messages are not identities, not automations, and not resources. They are connection configurations that define outbound targets and authentication methods.

3. Expand the credential type to include OAuth Providers and Profiles.

ServiceNow OAuth entities are credential infrastructure, not identities. They should be modeled as credential nodes.

4. Add three new edge types: USES_CONNECTION, AUTHENTICATES_AS, and TARGETS.

These edges complete the connection layer of the graph, enabling precise traversal from automation to identity through the connection and credential layers.

5. Enforce edge validity constraints at ingestion time.

The type system should prevent semantically invalid edges (e.g., BusinessRule -[HAS_ROLE]-> Role) through source/target type validation on each edge type.

6. Update the path materializer to start from both identity and automation nodes.

Automations get execution paths by following RUNS_AS to their delegated identity, then traversing the identity's authorization chain.

7. Create new finding types that leverage the type separation.

broken_execution_chain, orphaned_automation_active_identity, connection_without_credential, and credential_shared_across_chains are all findings that become possible only with correct typing.

8. Implement in 4 phases: type system, connector, UI, documentation.

Total estimated effort: 52-76 hours. No migration needed (no active clients).


Round 5 analysis complete. The founder's instinct is correct: a Business Rule is not an identity, a Script Include is not an identity, a REST Message is not an identity, and an OAuth Profile is not an identity. The current model's conflation of these concepts under the autonomous_identity umbrella produces semantically incorrect graphs, imprecise findings, inflated threat counts, and confused queries. The corrected 9-type model separates identity, automation, connection, and credential concerns, enabling precise traversal, accurate findings, and honest threat surface reporting. With no active clients and freedom to rewrite, there is no reason to perpetuate the semantic error.