Skip to main content

OAA Mapping Analysis — Architect

Date: 2026-02-13 (Round 4) Role: Systems Architect Core Question: How do SecurityV0 automation chains map to the Veza OAA data model, and what does alignment require?


Table of Contents

  1. Executive Summary
  2. OAA Data Model Recap
  3. Approach A: Automation as OAA Application
  4. Approach B: Automation as OAA Resource
  5. The Trigger/Data Problem
  6. NormalizedNodeType Evolution
  7. Reconciling execution_chains with OAA
  8. Neo4j Portability
  9. Concrete AzureGraphRouter Mapping
  10. Recommendation

1. Executive Summary

The founder's question cuts to the heart of a real tension: OAA's world is divided into Applications (top-level containers with users, roles, permissions, resources) and Resources (nestable data/system components). SecurityV0's automation chains — sequences like BusinessRule -> ScriptInclude -> RESTMessage -> OAuthProfile -> ServicePrincipal -> Entra — do not fit cleanly into either category.

Finding: Neither pure "automation-as-application" nor pure "automation-as-resource" is correct. The answer is a hybrid model where:

  • The source platform (ServiceNow) is the OAA Application
  • Automation artifacts (Business Rules, Flows, Scheduled Jobs) are typed Resources within that Application
  • Execution chains are a SecurityV0-specific composition layer that OAA has no concept of
  • The execution_chains collection from Round 3 remains necessary and complementary to OAA alignment

OAA models static permission snapshots. SecurityV0 models dynamic execution authority over time. These are different problems. OAA alignment gives us interoperability for permission exports. The execution_chains collection gives us the temporal, compositional intelligence that OAA cannot express.


2. OAA Data Model Recap

2.1 Core Entities

OAA defines a four-tier authorization hierarchy:

Identity Provider (IdP)
└── Identity (IdP User)
└── linked to → Local User (in Application)

Application
├── Local Users
├── Local Groups
├── Local Roles (bundle permissions)
├── Custom Permissions (mapped to 10 canonical types)
└── Resources (nestable via sub-resources)
└── Sub-Resources (arbitrary depth)

2.2 The 10 Canonical Permissions

PermissionCategoryDescription
DataReadDataRead data records
DataWriteDataCreate or modify data records
DataDeleteDataRemove data records
DataCreateDataCreate new data records
MetadataReadMetadataRead configuration/schema
MetadataWriteMetadataModify configuration/schema
NonDataNon-dataActions that do not touch data (e.g., login)
OwnershipAssignmentAdminAssign ownership of resources
ResourceAdminAdminAdminister resource configuration
AccountAdminAdminAdminister user accounts

2.3 OAA Patterns from Community Connectors

Studying the community connectors reveals consistent patterns:

ConnectorApplicationResourcesSub-ResourcesKey Pattern
GitHubGithub - {org}Repositories (repository)NoneTeams as groups, branch protection as properties
JiraJira - {instance}Projects (project)NonePermission schemes mapped per-project to roles
PagerDutyPagerDuty - {subdomain}Teams (team)NoneGlobal roles + team-scoped roles
BitbucketBitbucket - {workspace}Projects (Project)Repos (Repo)Hierarchical: Project > Repo
GitLabGitLab - {instance}Groups (group)Projects (project)Nested groups via sub-resources
LookerLooker - {instance}Model SetsModels > Connections3-level hierarchy with DB connections

Key observations:

  1. Each source platform becomes one OAA Application
  2. Resources represent data containers or organizational units, never executable logic
  3. Sub-resources model containment hierarchies (project > repo, group > project)
  4. Permissions flow from Identity -> Role -> Permission -> Resource
  5. There is no concept of "this resource triggers execution" or "this resource is an input to a process"

2.4 What OAA Does Not Model

ConceptOAA SupportSecurityV0 Need
Execution direction (input -> process -> output)NoneCritical
Temporal change trackingNone (snapshot-based)Critical
Cross-system authentication chainsPartial (IdP linkage)Critical
Automation as executable entityNoneCritical
Execution evidence (proof of action)NoneCritical
Ownership decay / orphaned NHINoneCritical
Composition (chain of artifacts)NoneCritical

3. Approach A: Automation as OAA Application

3.1 Design Rationale

In this approach, each execution chain becomes its own OAA Application. The Business Rule (or Flow, or Scheduled Job) is the top-level Application, and its dependencies become Resources within it.

This follows the intuition that an automation chain is a self-contained "application" with its own identity, configuration, and permissions.

3.2 Complete Schema

{
"applications": [
{
"name": "AzureGraphRouter - Incident Routing",
"application_type": "servicenow_automation",
"description": "Business Rule triggered on incident insert that routes data to Microsoft Graph API via OAuth",
"custom_properties": {
"anchor_entity_id": "br-sys-id-azuregraphrouter",
"chain_id": "chain-sha256-abc123",
"trigger_table": "incident",
"trigger_condition": "insert",
"egress_category": "external",
"destination_domain": "graph.microsoft.com",
"execution_mode": "autonomous",
"security_relevance": "active_external",
"ownership_status": "orphaned",
"first_detected_at": "2026-02-12T00:00:00Z",
"last_seen_at": "2026-02-13T00:00:00Z",
"composition_hash": "sha256:abc123..."
},
"local_users": [
{
"name": "sn-integration-user",
"unique_id": "sys-user-id-sn-integration",
"identities": [],
"is_active": true,
"custom_properties": {
"identity_type": "machine_account",
"source_system": "servicenow",
"run_as_type": "configured"
}
},
{
"name": "sp-azure-graph-router",
"unique_id": "entra-sp-id-azure-graph",
"identities": ["sp-azure-graph-router@tenant.onmicrosoft.com"],
"is_active": true,
"custom_properties": {
"identity_type": "service_principal",
"source_system": "entra_id",
"auth_protocol": "oauth2"
}
}
],
"local_groups": [],
"local_roles": [
{
"name": "ServiceNow Execution Role",
"unique_id": "sn-exec-role",
"permissions": ["sn_incident_read", "sn_incident_write"]
},
{
"name": "Entra Graph API Role",
"unique_id": "entra-graph-role",
"permissions": ["graph_user_read", "graph_group_readwrite"]
}
],
"resources": [
{
"name": "incident table (trigger)",
"resource_type": "trigger_source",
"description": "ServiceNow incident table - triggers this automation on insert",
"custom_properties": {
"table_name": "incident",
"role_in_chain": "trigger",
"sensitivity": "internal",
"business_domain": "it_ops"
}
},
{
"name": "ScriptInclude: AzureGraphHelper",
"resource_type": "script_component",
"description": "Script Include that implements Graph API call logic",
"custom_properties": {
"sys_id": "si-sys-id-azuregraphhelper",
"role_in_chain": "executor"
}
},
{
"name": "REST Message: MSGraphAPI",
"resource_type": "outbound_endpoint",
"description": "REST Message configuration targeting graph.microsoft.com",
"custom_properties": {
"endpoint": "https://graph.microsoft.com/v1.0",
"http_method": "POST",
"role_in_chain": "outbound_target"
}
},
{
"name": "OAuth Profile: AzureADOAuth",
"resource_type": "auth_credential",
"description": "OAuth 2.0 client credentials profile for Azure AD",
"custom_properties": {
"grant_type": "client_credentials",
"token_url": "https://login.microsoftonline.com/tenant-id/oauth2/v2.0/token",
"role_in_chain": "auth_credential"
}
},
{
"name": "Microsoft Graph API (destination)",
"resource_type": "destination_resource",
"description": "External resource accessed by this automation",
"custom_properties": {
"domain": "graph.microsoft.com",
"sensitivity": "confidential",
"business_domain": "identity_platform",
"role_in_chain": "destination"
}
}
]
}
],
"permissions": [
{
"name": "sn_incident_read",
"permission_type": ["DataRead"],
"custom_properties": {
"source_system": "servicenow",
"scope": "incident"
}
},
{
"name": "sn_incident_write",
"permission_type": ["DataWrite"],
"custom_properties": {
"source_system": "servicenow",
"scope": "incident"
}
},
{
"name": "graph_user_read",
"permission_type": ["DataRead"],
"custom_properties": {
"source_system": "entra_id",
"scope": "User.Read.All"
}
},
{
"name": "graph_group_readwrite",
"permission_type": ["DataRead", "DataWrite"],
"custom_properties": {
"source_system": "entra_id",
"scope": "Group.ReadWrite.All"
}
}
],
"identity_to_permissions": [
{
"identity": "sn-integration-user",
"identity_type": "local_user",
"application_permissions": [
{
"application": "AzureGraphRouter - Incident Routing",
"resources": [
{
"resource": "incident table (trigger)",
"permission": "sn_incident_read"
},
{
"resource": "incident table (trigger)",
"permission": "sn_incident_write"
}
]
}
]
},
{
"identity": "sp-azure-graph-router",
"identity_type": "local_user",
"application_permissions": [
{
"application": "AzureGraphRouter - Incident Routing",
"resources": [
{
"resource": "Microsoft Graph API (destination)",
"permission": "graph_user_read"
},
{
"resource": "Microsoft Graph API (destination)",
"permission": "graph_group_readwrite"
}
]
}
]
}
]
}

3.3 Mapping to NormalizedGraph Types

OAA ConceptNormalizedGraph MappingNotes
ApplicationNo direct mapping — would need new NormalizedNodeType: "application"The chain itself has no current node type
Local Userautonomous_identity nodesSP and integration user are identities
Local Rolerole nodesMaps directly
Permissionpermission nodesMaps directly
Resourceresource nodesSome new: trigger_source, script_component, outbound_endpoint
Custom PropertiesNormalizedNode.propertiesArbitrary key-value
Identity linkageAUTHENTICATES_TO edgesOAA IdP link = our cross-system auth

3.4 Problems with This Approach

Problem 1: Application Proliferation. A single ServiceNow instance with 50 Business Rules would generate 50 OAA Applications. OAA Applications are top-level containers — Veza's UI and query model assumes tens to hundreds of applications, not thousands. Every OAA connector in the community creates ONE application per platform instance.

Problem 2: Shared Resources Duplicated. The incident table is both a trigger source for this automation AND a resource in the ServiceNow application. Under this model, it appears as a Resource in the ServiceNow Application AND in 12 automation Applications that are triggered by it. Permissions on duplicated resources become inconsistent.

Problem 3: Cross-system Identity as "Local User" is Semantically Wrong. The Entra Service Principal is not a "local user" of the Business Rule. It is an external identity that the chain authenticates to. Forcing it into the local_users array loses the cross-system semantics.

Problem 4: No Execution Direction. OAA permissions are bidirectional — "this identity has DataRead on this resource." But in the automation chain, the incident table is an INPUT (trigger) and the Graph API is an OUTPUT (destination). The permission model cannot distinguish these roles.

Problem 5: Composition is Implicit. The chain's structure (BR calls SI, SI calls REST, REST uses OAuth) is not expressible in OAA's flat Resources list. The ordering and dependency relationships are lost — they can only be hinted at via custom properties like role_in_chain.

3.5 Verdict

Automation-as-Application is architecturally incorrect. It violates OAA's design intent (one Application per platform), duplicates resources, loses cross-system semantics, and cannot express execution direction. It might look reasonable for one chain but breaks down at scale.


4. Approach B: Automation as OAA Resource

4.1 Design Rationale

In this approach, ServiceNow is the OAA Application (following the pattern from every community connector). Automation artifacts are Resources within ServiceNow. The resource hierarchy models containment: ServiceNow App -> Automation Type -> Specific Automation -> Sub-components.

4.2 Complete Schema

{
"applications": [
{
"name": "ServiceNow - corp.service-now.com",
"application_type": "ServiceNow",
"description": "ServiceNow ITSM/ITOM platform instance",
"custom_properties": {
"instance_url": "https://corp.service-now.com",
"instance_id": "corp",
"connector_version": "1.0.0"
},
"local_users": [
{
"name": "sn-integration-user",
"unique_id": "sys-user-id-sn-integration",
"identities": [],
"is_active": true,
"custom_properties": {
"identity_type": "machine_account",
"user_name": "sn-integration-user",
"source_system": "servicenow"
}
}
],
"local_groups": [
{
"name": "itil",
"unique_id": "group-itil"
}
],
"local_roles": [
{
"name": "itil",
"unique_id": "role-itil",
"permissions": ["incident_read", "incident_write", "task_read", "task_write"]
},
{
"name": "hr_agent_workspace",
"unique_id": "role-hr-agent",
"permissions": ["hr_case_read", "hr_case_write"]
}
],
"resources": [
{
"name": "Business Rules",
"resource_type": "automation_category",
"description": "ServiceNow Business Rules - server-side scripts triggered by table events",
"sub_resources": [
{
"name": "AzureGraphRouter",
"resource_type": "business_rule",
"description": "Routes incident data to Microsoft Graph API on insert",
"custom_properties": {
"sys_id": "br-sys-id-azuregraphrouter",
"table": "incident",
"trigger_condition": "insert",
"execution_mode": "autonomous",
"security_relevance": "active_external",
"active": true,
"run_as": "sn-integration-user",
"egress_category": "external",
"destination_domain": "graph.microsoft.com"
},
"sub_resources": [
{
"name": "ScriptInclude: AzureGraphHelper",
"resource_type": "script_include",
"custom_properties": {
"sys_id": "si-sys-id-azuregraphhelper",
"api_name": "AzureGraphHelper",
"callable_from": "server"
}
},
{
"name": "REST Message: MSGraphAPI",
"resource_type": "rest_message",
"custom_properties": {
"sys_id": "rm-sys-id-msgraph",
"endpoint": "https://graph.microsoft.com/v1.0",
"http_methods": ["GET", "POST"],
"authentication": "oauth2"
},
"sub_resources": [
{
"name": "OAuth Profile: AzureADOAuth",
"resource_type": "oauth_profile",
"custom_properties": {
"sys_id": "oauth-sys-id-azuread",
"grant_type": "client_credentials",
"token_url": "https://login.microsoftonline.com/tenant/oauth2/v2.0/token",
"client_id": "a1b2c3d4-e5f6-..."
}
}
]
}
]
}
]
},
{
"name": "Flow Designer Flows",
"resource_type": "automation_category",
"description": "ServiceNow Flow Designer automations",
"sub_resources": []
},
{
"name": "Scheduled Jobs",
"resource_type": "automation_category",
"description": "ServiceNow scheduled jobs and cron-style executions",
"sub_resources": []
},
{
"name": "Tables",
"resource_type": "data_category",
"description": "ServiceNow tables accessible by identities",
"sub_resources": [
{
"name": "incident",
"resource_type": "table",
"custom_properties": {
"business_domain": "it_ops",
"sensitivity": "internal",
"contains_pii": false
}
},
{
"name": "hr_case",
"resource_type": "table",
"custom_properties": {
"business_domain": "hr",
"sensitivity": "confidential",
"contains_pii": true
}
}
]
}
]
}
],
"permissions": [
{
"name": "incident_read",
"permission_type": ["DataRead"]
},
{
"name": "incident_write",
"permission_type": ["DataWrite"]
},
{
"name": "hr_case_read",
"permission_type": ["DataRead"]
},
{
"name": "hr_case_write",
"permission_type": ["DataWrite"]
},
{
"name": "automation_execute",
"permission_type": ["NonData"],
"custom_properties": {
"description": "Ability to trigger or execute automation logic"
}
}
],
"identity_to_permissions": [
{
"identity": "sn-integration-user",
"identity_type": "local_user",
"application_permissions": [
{
"application": "ServiceNow - corp.service-now.com",
"resources": [
{
"resource": "incident",
"permission": "incident_read"
},
{
"resource": "incident",
"permission": "incident_write"
},
{
"resource": "AzureGraphRouter",
"permission": "automation_execute"
}
]
}
]
}
]
}

4.3 Resource Hierarchy Diagram

ServiceNow Application (corp.service-now.com)
├── Business Rules (automation_category)
│ ├── AzureGraphRouter (business_rule)
│ │ ├── ScriptInclude: AzureGraphHelper (script_include)
│ │ └── REST Message: MSGraphAPI (rest_message)
│ │ └── OAuth Profile: AzureADOAuth (oauth_profile)
│ ├── IncidentNotify (business_rule)
│ │ └── ...
│ └── CMDBSync (business_rule)
│ └── ...
├── Flow Designer Flows (automation_category)
│ ├── HR Onboarding Workflow (flow_designer_flow)
│ │ └── ...
│ └── ...
├── Scheduled Jobs (automation_category)
│ └── ...
└── Tables (data_category)
├── incident (table)
├── hr_case (table)
├── sys_user (table)
└── ...

4.4 Interaction with Existing Entity Model

The automation-as-resource approach has cleaner alignment with SecurityV0's existing entity model:

OAA Resource TypeSecurityV0 Entity TypeNormalizedNodeTypeRelationship
business_ruleIdentity (identity_type: business_rule)autonomous_identityThe same entity, different representation
script_includeIdentity or Resource (depends on context)autonomous_identity or resourceScript includes are callable code
rest_messageResource (outbound endpoint)resourceConfiguration artifact
oauth_profileCredentialcredentialAuthentication material
tableResourceresourceData container
flow_designer_flowIdentity (identity_type: flow_designer_flow)autonomous_identityExecutable automation

Critical tension: In OAA, a Business Rule is a Resource. In SecurityV0, it is an Identity (because it executes autonomously). This is the core type-mapping challenge. See Section 6 for resolution.

4.5 Advantages Over Approach A

FactorApproach A (as App)Approach B (as Resource)
OAA conventionViolates (1 app per platform)Follows (1 app per platform)
Resource duplicationincident table in 12 appsincident table once, shared
Scalability50 BRs = 50 applications50 BRs = 50 sub-resources
Cross-system authForced into local_usersExternal IdP link (correct)
Hierarchy expressionFlat resources listNested sub-resources
Community patternNo precedentMatches Bitbucket, GitLab, Looker

4.6 Remaining Problems

Problem 1: Automation is Both Identity and Resource. A Business Rule executes (it is an identity/actor) AND is configured/managed (it is a resource/artifact). OAA only sees the resource dimension. SecurityV0 needs both dimensions.

Problem 2: Cross-System Chain Not Expressible. The chain extends from ServiceNow to Entra ID. In OAA, these are two separate Applications. The link between the OAuth Profile (in ServiceNow) and the Service Principal (in Entra) is not a resource hierarchy — it is a cross-application identity binding. OAA handles this via IdP linkage, but only for users, not for resources.

Problem 3: Execution Evidence Has No Home. OAA does not model "this resource executed at time T." Execution evidence is entirely outside OAA's scope.

Problem 4: Still No Execution Direction. The resource hierarchy shows containment (BR contains SI, SI uses REST Message), but does not express "incident table event triggers BR, which reads incident data, then writes to Graph API."


5. The Trigger/Data Problem

5.1 The Three Roles of the Incident Table

The founder identifies a deep problem: the incident table plays three distinct roles in the automation:

Role 1: EVENT SOURCE (Trigger)
incident table --[emits INSERT event]--> Business Rule fires

Role 2: DATA SOURCE (DataRead)
Business Rule --[reads incident fields]--> incident table
(the BR reads the incident record that triggered it)

Role 3: DATA TARGET (DataWrite — in another chain)
Some other automation --[writes/updates]--> incident table
(the table is also a write target for other automations)

5.2 How OAA Models This

In OAA's permission model, the incident table is just a Resource. Permissions are:

sn-integration-user → incident_read → incident table   (DataRead)
sn-integration-user → incident_write → incident table (DataWrite)

This tells us the identity CAN read and write the incident table. It does NOT tell us:

  • That the read happens because an event triggered the automation
  • That the write is the output of a different automation
  • Which direction the data flows
  • What caused the read vs. what the read enables

5.3 Why OAA's Permission Model Cannot Capture Execution Direction

OAA permissions are capability assertions: "identity X has permission P on resource R." They answer: "Who can do what to what?"

Execution chains require flow assertions: "event on resource A triggers process B, which reads A, transforms data, and writes to resource C." They answer: "What happens when, and through what chain of authority?"

These are fundamentally different questions:

DimensionOAA PermissionSecurityV0 Execution Chain
DirectionBidirectional capabilityDirected flow (input -> process -> output)
TriggerNot modeledExplicit (event, schedule, API call)
Causality"Can do""Does do because of"
CompositionFlat (identity -> resource)Ordered (trigger -> executor -> target)
TimeSnapshotTemporal (when did this chain execute?)

5.4 Extensions Needed for Execution Direction

To represent execution flow within OAA's framework, we would need:

Extension 1: Directional Permission Types

Current OAA:     DataRead, DataWrite, DataDelete, ...
Extension: DataRead_AsInput, DataWrite_AsOutput, EventTrigger, ExecuteLogic

This would require new canonical permission types:

Extended PermissionMeaningExample
EventTriggerResource emits an event that starts this processincident table -> BR
DataRead_AsInputData is read as input to processingBR reads incident fields
DataWrite_AsOutputData is written as output of processingBR writes to Graph API
ExecuteLogicResource contains executable logic that runsScriptInclude code runs
InvokeEndpointOutbound call to external systemREST Message -> graph.microsoft.com

Extension 2: Resource Relationships (Currently Not in OAA)

OAA resources have parent-child containment (sub_resources). They do NOT have peer relationships. We would need:

incident_table --[TRIGGERS]--> AzureGraphRouter_BR
AzureGraphRouter_BR --[CALLS]--> AzureGraphHelper_SI
AzureGraphHelper_SI --[INVOKES]--> MSGraphAPI_REST
MSGraphAPI_REST --[AUTHENTICATES_VIA]--> AzureADOAuth_Profile
AzureADOAuth_Profile --[BINDS_TO]--> sp-azure-graph-router (Entra SP)

These are not permission relationships. They are structural/execution relationships between resources.

Extension 3: Resource Role Annotations

Each resource in a chain has a role:

{
"resource": "incident table",
"chain_roles": ["trigger_source", "data_input"],
"in_chain": "AzureGraphRouter"
}

5.5 Practical Resolution

OAA cannot be extended (it is Veza's proprietary format). Therefore:

  1. For OAA export: Model the incident table as a Resource with standard DataRead/DataWrite permissions. Accept that directional flow is lost in the OAA representation.

  2. For SecurityV0 internal model: Use the existing relationship types (TRIGGERS_ON, RUNS_AS, AUTHENTICATES_TO, EXECUTES_ON) to model directional flow. These relationships carry the semantics OAA cannot.

  3. For the execution_chains collection: Store the ordered, directional chain with role annotations per entity. This is where the "trigger -> process -> output" flow lives.

The incident table appearing in three roles is resolved by SecurityV0's edge types:

incident_table <--[TRIGGERS_ON]-- AzureGraphRouter_BR     (Role 1: trigger)
incident_table <--[EXECUTES_ON]-- sn-integration-user (Role 2: data access)
incident_table <--[EXECUTES_ON]-- other-automation (Role 3: write target)

Each edge type carries different semantics. OAA's flat permission model collapses all three into "DataRead" and "DataWrite."


6. NormalizedNodeType Evolution

6.1 Current Type Hierarchy

export type NormalizedNodeType =
| "autonomous_identity" // Service principals, OAuth apps, automations
| "human_identity" // Owners (humans, teams, BUs)
| "role" // Permission groupings
| "permission" // Atomic capabilities
| "resource" // Data/system targets
| "credential" // Authentication material
| "execution_evidence"; // Proof of execution

This maps to the internal entity_type enum:

  • autonomous_identity -> identity
  • human_identity -> owner
  • role -> role
  • permission -> permission
  • resource -> resource
  • credential -> credential
  • execution_evidence -> stored in execution_evidence collection

6.2 OAA Type Mapping

OAA TypeCurrent SecurityV0 TypeAlignment
ApplicationNo equivalentGap
Local Userautonomous_identityPartial (OAA user != NHI)
Local GroupNo equivalent (implicit in roles)Gap
Local RoleroleDirect
PermissionpermissionDirect
ResourceresourceDirect
Sub-Resourceresource (flat)Needs hierarchy
IdP Userhuman_identity or autonomous_identityDepends on context

6.3 Proposed NormalizedNodeType Evolution

The goal is to align with OAA concepts without losing SecurityV0's execution tracking semantics.

Option 1: Add OAA Types as Separate Node Types

export type NormalizedNodeType =
// SecurityV0 core types (unchanged)
| "autonomous_identity"
| "human_identity"
| "role"
| "permission"
| "resource"
| "credential"
| "execution_evidence"
// OAA alignment types (new)
| "application" // OAA Application container
| "group"; // OAA Local Group

Assessment: Minimal change. application is useful as a container concept. group may duplicate role semantics. The problem is that application in OAA means something different from SecurityV0's needs — OAA Application is a top-level platform, not an automation chain.

Option 2: Introduce Subtype Discrimination (Recommended)

Keep the current type enum stable. Use properties.subtype for finer-grained discrimination:

// NormalizedNodeType stays the same — 7 types
export type NormalizedNodeType =
| "autonomous_identity"
| "human_identity"
| "role"
| "permission"
| "resource"
| "credential"
| "execution_evidence";

// Subtype discrimination via properties
interface IdentitySubtypes {
identity_type:
| "service_principal"
| "oauth_app"
| "github_app"
| "pat"
| "agent"
| "machine_account"
| "flow_designer_flow"
| "business_rule"
| "scheduled_job"
| "system_execution";
}

interface ResourceSubtypes {
resource_type:
| "table"
| "module"
| "api_endpoint"
| "repository"
| "secret"
| "workflow"
| "cloud_storage"
// OAA-aligned additions
| "automation_component" // script_include, rest_message (non-executable artifacts)
| "auth_config" // oauth_profile, SAML config (credential-adjacent config)
| "application_container"; // Represents an OAA Application boundary
}

Assessment: This preserves the existing 7-type model (no breaking changes to queries, filters, SCIM exports, UI). It adds semantic richness through properties. The application_container resource subtype lets us represent OAA Application boundaries without promoting Application to a top-level node type.

Option 3: Full OAA Alignment (Maximum Change)

export type NormalizedNodeType =
| "identity" // Merges autonomous_identity + human_identity
| "application" // OAA Application (new)
| "group" // OAA Local Group (new)
| "role" // Unchanged
| "permission" // Unchanged
| "resource" // Unchanged
| "credential" // Unchanged
| "execution_evidence" // SecurityV0-specific (no OAA equivalent)
| "execution_chain"; // SecurityV0-specific (no OAA equivalent)

Assessment: Breaking change. Merging autonomous_identity and human_identity into identity loses the most important discrimination in the platform (NHI vs human). Adding application and group complicates the entity model without clear benefit — OAA Applications map better to SecurityV0's tenant + source_system concept than to a new entity type. Adding execution_chain resurrects the Option D debate from Round 3 (virtual entity).

6.4 Recommendation: Option 2

Keep the 7-type NormalizedNodeType enum. Add subtype richness via properties:

// No enum change. Type stability preserved.
// Additions are property-level:

// For OAA export, map existing types:
const oaaTypeMapping: Record<NormalizedNodeType, string> = {
"autonomous_identity": "local_user", // OAA local user (for NHIs acting within an app)
"human_identity": "idp_identity", // OAA IdP user (for human owners)
"role": "local_role", // Direct mapping
"permission": "custom_permission", // Direct mapping
"resource": "resource", // Direct mapping
"credential": "resource", // OAA has no credential type; model as resource
"execution_evidence": null, // No OAA equivalent; exclude from export
};

This keeps SecurityV0's type system clean while enabling OAA-compatible exports through a mapping layer.

6.5 Dual-Identity Problem: When an Entity Is Both Identity and Resource

The founder's insight is that some entities (Business Rule, Script Include) are simultaneously:

  • Identities (they execute, they have permissions, they run-as)
  • Resources (they are configured, managed, versioned, have ACLs)

In SecurityV0's current model, these are autonomous_identity nodes. For OAA export, they become Resources (since OAA has no concept of a resource that executes).

Resolution: Entity Projection

A single SecurityV0 entity can project differently depending on the export target:

EntitySecurityV0 InternalOAA ExportNeo4j Export
Business Ruleautonomous_identityresource (business_rule):Identity:Automation:BusinessRule
Script Includeautonomous_identity or resourceresource (script_include):Resource:ScriptComponent
REST Messageresourceresource (rest_message):Resource:OutboundEndpoint
OAuth Profilecredentialresource (auth_config):Credential:OAuthProfile
Service Principalautonomous_identitylocal_user:Identity:ServicePrincipal

This projection is handled at the export layer, not the storage layer. The entity collection stores the canonical SecurityV0 representation. Export functions transform as needed.


7. Reconciling execution_chains with OAA

7.1 The Core Question

Round 3 established that an execution_chains collection is needed for chain identity, listing, and temporal tracking. Now we ask: does OAA alignment replace this need?

Answer: No. The two serve completely different purposes.

7.2 What Each Provides

Capabilityexecution_chains CollectionOAA-Aligned Entity Model
Stable chain identityYes (chain_id from anchor)No (OAA has no chain concept)
Chain listing / searchYes (direct collection query)No
Chain-level temporal diffYes (composition_hash comparison)No (OAA is snapshot-only)
Chain-level findingsYes (orphaned chain, blast radius expansion)No
Permission export to VezaNoYes
Standard-based interopNoYes (OAA JSON format)
Resource hierarchyNo (references entity IDs)Yes (sub-resource nesting)
Identity-to-resource permission mappingNoYes

7.3 Coexistence Architecture

                        ┌─────────────────────────────┐
│ Source Systems │
│ (Entra, ServiceNow, ...) │
└─────────────┬───────────────┘

NormalizedGraph

┌─────────────▼───────────────┐
│ sv0-platform Ingestion │
│ (sync-ingestion.ts) │
└─────────────┬───────────────┘

┌─────────────────┼─────────────────┐
│ │ │
┌─────────▼──────┐ ┌───────▼────────┐ ┌──────▼──────────┐
│ entities │ │execution_chains│ │ OAA Export │
│ collection │ │ collection │ │ (on-demand) │
│ (core model) │ │ (composition) │ │ │
└────────────────┘ └────────────────┘ └─────────────────┘
│ │ │
│ │ │
Graph queries Chain queries Veza API push
Findings Chain-level findings Permission snapshots
Temporal views Chain temporal diff Standards-based export
Evidence packs Chain evidence packs

7.4 Data Flow Between the Three Layers

Step 1: Ingest (NormalizedGraph -> entities)

  • Connector discovers entities and relationships
  • Platform upserts entities, computes execution paths
  • This is unchanged from current implementation

Step 2: Chain Assembly (entities -> execution_chains)

  • After path materialization, chain builder runs
  • BFS from anchor entities (BR, Flow, Job) following edge types
  • Creates/updates execution_chain documents with composition, summary, fingerprint
  • This is Round 3's Phase 1 deliverable

Step 3: OAA Export (entities + execution_chains -> OAA JSON)

  • On-demand export endpoint: GET /api/v1/export/oaa/:source_system
  • Reads entities for the source system
  • Maps to OAA Application + Resources + Users + Permissions
  • Automation entities (BR, Flow, Job) become Resources with sub-resource hierarchies
  • Chain metadata from execution_chains enriches custom_properties
  • Output: OAA-compatible JSON payload

7.5 Reconciliation Points

Where execution_chains and OAA-aligned entities overlap, the data must be consistent:

FieldSource of TruthOAA Representation
Entity identity (sys_id, etc.)entities collectionResource unique_id
Entity relationshipsentities.relationships[]Sub-resource hierarchy
Chain compositionexecution_chains.entity_refs[]Custom property on anchor Resource
Chain summary (egress, blast radius)execution_chains.summaryCustom properties
Permissionsentities (role/permission nodes)OAA permissions + identity_to_permissions
Execution evidenceexecution_evidence collectionNot exported to OAA
Temporal historyentity_versions + eventsNot exported to OAA

7.6 OAA Export API Design

// New endpoint: GET /api/v1/export/oaa/:sourceSystem
interface OAAExportOptions {
sourceSystem: string; // e.g., "servicenow"
includeChainMetadata: boolean; // Enrich resources with chain summary data
includeInactiveEntities: boolean;
format: "json" | "csv";
}

interface OAAExportResult {
applications: OAAApplication[];
permissions: OAAPermission[];
identity_to_permissions: OAAIdentityPermission[];
export_metadata: {
exported_at: string;
entity_count: number;
chain_count: number;
schema_version: string;
};
}

This endpoint generates OAA-compatible output from SecurityV0's richer internal model. Data flows one way: SecurityV0 -> OAA. We never import OAA data into SecurityV0.


8. Neo4j Portability

8.1 Approach A: Automation-as-Application in Neo4j

If automations are modeled as Applications:

// Node labels
(:Application {name: "AzureGraphRouter - Incident Routing", type: "servicenow_automation"})
(:LocalUser {name: "sn-integration-user", identity_type: "machine_account"})
(:LocalUser {name: "sp-azure-graph-router", identity_type: "service_principal"})
(:Resource {name: "incident table", resource_type: "trigger_source"})
(:Resource {name: "Microsoft Graph API", resource_type: "destination_resource"})
(:Permission {name: "sn_incident_read", canonical: "DataRead"})

// Edges
(app)-[:HAS_LOCAL_USER]->(user)
(app)-[:HAS_RESOURCE]->(resource)
(user)-[:HAS_PERMISSION {resource: "incident table"}]->(perm)

CISO Query: "Show me all automations that gained access to sensitive data in the last 30 days"

// This is awkward — automations are Application nodes, not Identity nodes
MATCH (app:Application {type: "servicenow_automation"})
MATCH (app)-[:HAS_RESOURCE]->(r:Resource)
WHERE r.sensitivity IN ["confidential", "restricted"]

// "Gained access" requires temporal — but OAA is snapshot-based
// We would need a separate temporal layer alongside the Application model
// This makes the query split across two conceptual layers

// Workaround: check custom_properties for temporal fields
WHERE app.last_seen_at > datetime() - duration('P30D')
AND r.sensitivity IN ["confidential", "restricted"]
RETURN app.name, collect(r.name) AS sensitive_resources

Assessment: Awkward. Automations as Application nodes do not participate in the same graph as Identity nodes. The CISO cannot query "identities AND automations" with a single traversal pattern.

8.2 Approach B: Automation-as-Resource in Neo4j

If automations are Resources within the ServiceNow Application:

// Node labels
(:Application {name: "ServiceNow - corp"})
(:Identity {name: "sn-integration-user", identity_type: "machine_account"})
(:Identity {name: "sp-azure-graph-router", identity_type: "service_principal"})
(:Resource {name: "AzureGraphRouter", resource_type: "business_rule"})
(:Resource {name: "AzureGraphHelper", resource_type: "script_include"})
(:Resource {name: "MSGraphAPI", resource_type: "rest_message"})
(:Resource {name: "AzureADOAuth", resource_type: "oauth_profile"})
(:Resource {name: "incident", resource_type: "table"})

// Edges
(app)-[:CONTAINS]->(br:Resource {resource_type: "business_rule"})
(br)-[:CONTAINS]->(si:Resource {resource_type: "script_include"})
(si)-[:CONTAINS]->(rm:Resource {resource_type: "rest_message"})
(rm)-[:CONTAINS]->(oauth:Resource {resource_type: "oauth_profile"})
(identity)-[:HAS_PERMISSION]->(br)
(identity)-[:EXECUTES_ON]->(table)

CISO Query:

// Automations are Resources — but "gained access" is about the identity chain
MATCH (br:Resource {resource_type: "business_rule"})
MATCH (br)<-[:RUNS_AS]-(identity:Identity)
MATCH (identity)-[:HAS_ROLE]->(:Role)-[:GRANTS]->(:Permission)-[:APPLIES_TO]->(r:Resource)
WHERE r.sensitivity IN ["confidential", "restricted"]
// Still need temporal layer for "gained in last 30 days"
RETURN br.name, identity.name, collect(r.name) AS sensitive_resources

Assessment: Better — automations participate in the graph via RUNS_AS. But the query still has to "jump" from the Resource (BR) to the Identity (via RUNS_AS) to traverse the permission graph. The automation itself is not directly in the permission path.

In SecurityV0's model, automations ARE identities. They participate directly in the execution path graph:

// Node labels — SecurityV0's model maps directly to labeled property graph
(:Identity:Automation {name: "AzureGraphRouter", identity_type: "business_rule",
execution_mode: "autonomous", security_relevance: "active_external"})
(:Identity:ServicePrincipal {name: "sp-azure-graph-router", source_system: "entra_id"})
(:Identity:MachineAccount {name: "sn-integration-user", source_system: "servicenow"})
(:Resource:Table {name: "incident", business_domain: "it_ops", sensitivity: "internal"})
(:Resource:ExternalAPI {name: "graph.microsoft.com", sensitivity: "confidential"})
(:Role {name: "itil"})
(:Permission {name: "incident.write", action: "update"})
(:Credential:OAuthProfile {name: "AzureADOAuth", grant_type: "client_credentials"})

// Execution chain edges — SecurityV0's relationship types
(br:Identity {identity_type: "business_rule"})-[:TRIGGERS_ON]->(incident:Resource)
(br)-[:RUNS_AS]->(sn_user:Identity {identity_type: "machine_account"})
(sn_user)-[:HAS_ROLE]->(role:Role)
(role)-[:GRANTS]->(perm:Permission)
(perm)-[:APPLIES_TO]->(incident)
(sn_user)-[:AUTHENTICATES_TO {auth_protocol: "oauth2"}]->(sp:Identity)
(sp)-[:HAS_ROLE]->(entra_role:Role)
(entra_role)-[:GRANTS]->(graph_perm:Permission)
(graph_perm)-[:APPLIES_TO]->(graph_api:Resource)

// Chain metadata node (from execution_chains collection)
(:ExecutionChain {chain_id: "chain-abc123", name: "AzureGraphRouter Incident Routing",
composition_hash: "sha256:...", first_detected_at: datetime("2026-02-12")})
(chain)-[:ANCHORED_BY]->(br)
(chain)-[:CONTAINS {role: "entry_point"}]->(br)
(chain)-[:CONTAINS {role: "executor"}]->(sn_user)
(chain)-[:CONTAINS {role: "destination_identity"}]->(sp)

CISO Query: "Show me all automations that gained access to sensitive data in the last 30 days"

// Direct, single-pattern query because automations ARE identities
MATCH (automation:Identity:Automation)-[:RUNS_AS|AUTHENTICATES_TO*1..3]->(via:Identity)
MATCH (via)-[:HAS_ROLE]->(:Role)-[:GRANTS]->(:Permission)-[:APPLIES_TO]->(r:Resource)
WHERE r.sensitivity IN ["confidential", "restricted"]

// Temporal: check for role grants in last 30 days
MATCH (via)-[hr:HAS_ROLE]->(role:Role)
WHERE hr.granted_at > datetime() - duration('P30D')
AND (role)-[:GRANTS]->(:Permission)-[:APPLIES_TO]->(r)

RETURN automation.name AS automation,
via.name AS acting_as,
collect(DISTINCT r.name) AS sensitive_resources,
hr.granted_at AS access_gained_at
ORDER BY hr.granted_at DESC

Alternative — Chain-level query:

// "Which execution chains have members that gained sensitive access recently?"
MATCH (chain:ExecutionChain)-[:CONTAINS]->(member:Identity)
MATCH (member)-[hr:HAS_ROLE]->(role:Role)-[:GRANTS]->(:Permission)-[:APPLIES_TO]->(r:Resource)
WHERE r.sensitivity IN ["confidential", "restricted"]
AND hr.granted_at > datetime() - duration('P30D')
RETURN chain.name AS chain_name,
chain.chain_id AS chain_id,
member.name AS entity_that_gained_access,
collect(r.name) AS sensitive_resources,
hr.granted_at AS when

8.4 Neo4j Portability Comparison

FactorApproach A (App)Approach B (Resource)Approach C (Hybrid)
Automation in permission traversalNo (separate graph)Via RUNS_AS hopDirect (is Identity)
CISO query complexity2+ patterns2 patterns1 pattern
Chain queryMatch Application nodesMatch Resource nodes + hopMatch Chain + CONTAINS
Multi-label support:Application:Resource:Identity:Automation:BusinessRule
Temporal integrationSeparate layerSeparate layerEdge properties + events
Cross-system traversalAcross Application boundariesAUTHENTICATES_TO between appsDirect AUTHENTICATES_TO
Reverse query ("what automations access HR?")Per-app searchResource search + RUNS_AS hopDirect traversal
// Constraints
CREATE CONSTRAINT entity_id IF NOT EXISTS FOR (e:Entity) REQUIRE e.entity_id IS UNIQUE;
CREATE CONSTRAINT chain_id IF NOT EXISTS FOR (c:ExecutionChain) REQUIRE c.chain_id IS UNIQUE;

// Indexes
CREATE INDEX identity_type IF NOT EXISTS FOR (i:Identity) ON (i.identity_type);
CREATE INDEX resource_sensitivity IF NOT EXISTS FOR (r:Resource) ON (r.sensitivity);
CREATE INDEX chain_anchor IF NOT EXISTS FOR (c:ExecutionChain) ON (c.anchor_entity_id);
CREATE INDEX automation_relevance IF NOT EXISTS FOR (a:Automation) ON (a.security_relevance);

// Multi-label pattern
// Each entity gets :Entity base label + type-specific labels
// Example: Identity(business_rule) -> :Entity:Identity:Automation:BusinessRule
// Example: Resource(table) -> :Entity:Resource:Table
// Example: Credential(oauth_profile) -> :Entity:Credential:OAuthProfile

9. Concrete AzureGraphRouter Mapping

9.1 The Chain

BusinessRule: AzureGraphRouter
├── triggered by: incident table (on insert)
├── calls: ScriptInclude: AzureGraphHelper
│ └── calls: REST Message: MSGraphAPI
│ └── authenticates via: OAuth Profile: AzureADOAuth
│ └── client_id matches: ServicePrincipal: sp-azure-graph-router (Entra)
│ └── has roles: User.Read.All, Group.ReadWrite.All
│ └── accesses: Microsoft Graph API (identity_platform domain)
└── runs as: sn-integration-user (ServiceNow)
└── has roles: itil, hr_agent_workspace
└── accesses: incident, hr_case tables

9.2 Approach A Document Structure (Automation-as-Application)

{
"oaa_export": {
"applications": [
{
"name": "AzureGraphRouter - Incident Routing",
"application_type": "servicenow_automation",
"description": "Business Rule: AzureGraphRouter. Triggered on incident insert. Routes data to Microsoft Graph API. Runs as sn-integration-user. Authenticates to Entra SP sp-azure-graph-router via OAuth2 client_credentials.",
"custom_properties": {
"chain_id": "chain-d4e5f6a7b8c9",
"anchor_sys_id": "br-sys-id-azuregraphrouter",
"source_system": "servicenow",
"instance_url": "https://corp.service-now.com",
"trigger_table": "incident",
"trigger_condition": "insert",
"egress_category": "external",
"destination_domain": "graph.microsoft.com",
"execution_mode": "autonomous",
"security_relevance": "active_external",
"ownership_status": "orphaned",
"composition_hash": "sha256:a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"entity_count": 7,
"cross_system": true,
"source_systems": ["servicenow", "entra_id"],
"blast_radius_domains": ["it_ops", "identity_platform"],
"max_sensitivity": "confidential",
"total_roles": 4,
"first_detected_at": "2026-02-12T00:00:00Z",
"last_seen_at": "2026-02-13T10:00:00Z"
},
"local_users": [
{
"name": "sn-integration-user",
"unique_id": "uuid-sn-integration-user",
"identities": [],
"is_active": true,
"custom_properties": {
"identity_type": "machine_account",
"source_system": "servicenow",
"source_id": "sys-user-sn-integration",
"role_in_chain": "executor",
"run_as_type": "configured"
}
},
{
"name": "sp-azure-graph-router",
"unique_id": "uuid-sp-azure-graph-router",
"identities": ["a1b2c3d4-e5f6-7890-abcd-ef1234567890@tenant.onmicrosoft.com"],
"is_active": true,
"custom_properties": {
"identity_type": "service_principal",
"source_system": "entra_id",
"source_id": "sp-id-azure-graph-router",
"role_in_chain": "destination_identity",
"appId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"servicePrincipalType": "Application"
}
}
],
"local_groups": [],
"local_roles": [
{
"name": "itil",
"unique_id": "role-itil",
"permissions": ["sn_incident_read", "sn_incident_write", "sn_task_read"]
},
{
"name": "hr_agent_workspace",
"unique_id": "role-hr-agent",
"permissions": ["sn_hr_case_read", "sn_hr_case_write"]
},
{
"name": "User.Read.All",
"unique_id": "role-entra-user-read",
"permissions": ["graph_user_read"]
},
{
"name": "Group.ReadWrite.All",
"unique_id": "role-entra-group-rw",
"permissions": ["graph_group_read", "graph_group_write"]
}
],
"resources": [
{
"name": "incident table",
"resource_type": "trigger_source",
"custom_properties": {
"table_name": "incident",
"role_in_chain": "trigger",
"business_domain": "it_ops",
"sensitivity": "internal",
"source_system": "servicenow"
}
},
{
"name": "ScriptInclude: AzureGraphHelper",
"resource_type": "script_component",
"custom_properties": {
"sys_id": "si-sys-id-azuregraphhelper",
"role_in_chain": "code_executor",
"source_system": "servicenow"
}
},
{
"name": "REST Message: MSGraphAPI",
"resource_type": "outbound_endpoint",
"custom_properties": {
"sys_id": "rm-sys-id-msgraph",
"endpoint": "https://graph.microsoft.com/v1.0",
"role_in_chain": "outbound_target",
"source_system": "servicenow"
}
},
{
"name": "OAuth Profile: AzureADOAuth",
"resource_type": "auth_credential",
"custom_properties": {
"sys_id": "oauth-sys-id-azuread",
"grant_type": "client_credentials",
"client_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"role_in_chain": "auth_credential",
"source_system": "servicenow"
}
},
{
"name": "hr_case table",
"resource_type": "data_target",
"custom_properties": {
"table_name": "hr_case",
"role_in_chain": "accessible_data",
"business_domain": "hr",
"sensitivity": "confidential",
"contains_pii": true,
"source_system": "servicenow"
}
},
{
"name": "Microsoft Graph API",
"resource_type": "destination_resource",
"custom_properties": {
"domain": "graph.microsoft.com",
"role_in_chain": "destination",
"business_domain": "identity_platform",
"sensitivity": "confidential",
"source_system": "entra_id"
}
}
]
}
],
"permissions": [
{"name": "sn_incident_read", "permission_type": ["DataRead"]},
{"name": "sn_incident_write", "permission_type": ["DataWrite"]},
{"name": "sn_task_read", "permission_type": ["DataRead"]},
{"name": "sn_hr_case_read", "permission_type": ["DataRead"]},
{"name": "sn_hr_case_write", "permission_type": ["DataWrite"]},
{"name": "graph_user_read", "permission_type": ["DataRead"]},
{"name": "graph_group_read", "permission_type": ["DataRead"]},
{"name": "graph_group_write", "permission_type": ["DataWrite"]}
],
"identity_to_permissions": [
{
"identity": "sn-integration-user",
"identity_type": "local_user",
"application_permissions": [
{
"application": "AzureGraphRouter - Incident Routing",
"resources": [
{"resource": "incident table", "permission": "sn_incident_read"},
{"resource": "incident table", "permission": "sn_incident_write"},
{"resource": "hr_case table", "permission": "sn_hr_case_read"},
{"resource": "hr_case table", "permission": "sn_hr_case_write"}
]
}
]
},
{
"identity": "sp-azure-graph-router",
"identity_type": "local_user",
"application_permissions": [
{
"application": "AzureGraphRouter - Incident Routing",
"resources": [
{"resource": "Microsoft Graph API", "permission": "graph_user_read"},
{"resource": "Microsoft Graph API", "permission": "graph_group_read"},
{"resource": "Microsoft Graph API", "permission": "graph_group_write"}
]
}
]
}
]
}
}

Document statistics:

  • 1 Application
  • 2 Local Users
  • 4 Local Roles
  • 6 Resources
  • 8 Permissions
  • 2 Identity-to-Permission mappings

9.3 Approach B Document Structure (Automation-as-Resource)

{
"oaa_export": {
"applications": [
{
"name": "ServiceNow - corp.service-now.com",
"application_type": "ServiceNow",
"description": "ServiceNow ITSM/ITOM platform",
"custom_properties": {
"instance_url": "https://corp.service-now.com",
"instance_id": "corp",
"automation_count": 15,
"active_chain_count": 3
},
"local_users": [
{
"name": "sn-integration-user",
"unique_id": "uuid-sn-integration-user",
"identities": [],
"is_active": true,
"custom_properties": {
"identity_type": "machine_account",
"user_name": "sn-integration-user"
}
}
],
"local_groups": [],
"local_roles": [
{
"name": "itil",
"unique_id": "role-itil",
"permissions": ["incident_read", "incident_write", "task_read"]
},
{
"name": "hr_agent_workspace",
"unique_id": "role-hr-agent",
"permissions": ["hr_case_read", "hr_case_write"]
}
],
"resources": [
{
"name": "Business Rules",
"resource_type": "automation_category",
"sub_resources": [
{
"name": "AzureGraphRouter",
"resource_type": "business_rule",
"description": "Triggered on incident insert. Routes to Graph API.",
"custom_properties": {
"sys_id": "br-sys-id-azuregraphrouter",
"table": "incident",
"trigger_condition": "insert",
"execution_mode": "autonomous",
"security_relevance": "active_external",
"active": true,
"run_as": "sn-integration-user",
"egress_category": "external",
"destination_domain": "graph.microsoft.com",
"chain_id": "chain-d4e5f6a7b8c9",
"chain_entity_count": 7,
"ownership_status": "orphaned"
},
"sub_resources": [
{
"name": "AzureGraphHelper",
"resource_type": "script_include",
"custom_properties": {
"sys_id": "si-sys-id-azuregraphhelper",
"api_name": "AzureGraphHelper",
"callable_from": "server"
}
},
{
"name": "MSGraphAPI",
"resource_type": "rest_message",
"custom_properties": {
"sys_id": "rm-sys-id-msgraph",
"endpoint": "https://graph.microsoft.com/v1.0",
"http_methods": ["GET", "POST"]
},
"sub_resources": [
{
"name": "AzureADOAuth",
"resource_type": "oauth_profile",
"custom_properties": {
"sys_id": "oauth-sys-id-azuread",
"grant_type": "client_credentials",
"client_id": "a1b2c3d4-...",
"token_url": "https://login.microsoftonline.com/tenant/oauth2/v2.0/token"
}
}
]
}
]
}
]
},
{
"name": "Tables",
"resource_type": "data_category",
"sub_resources": [
{
"name": "incident",
"resource_type": "table",
"custom_properties": {
"business_domain": "it_ops",
"sensitivity": "internal"
}
},
{
"name": "hr_case",
"resource_type": "table",
"custom_properties": {
"business_domain": "hr",
"sensitivity": "confidential",
"contains_pii": true
}
}
]
}
]
}
],
"permissions": [
{"name": "incident_read", "permission_type": ["DataRead"]},
{"name": "incident_write", "permission_type": ["DataWrite"]},
{"name": "task_read", "permission_type": ["DataRead"]},
{"name": "hr_case_read", "permission_type": ["DataRead"]},
{"name": "hr_case_write", "permission_type": ["DataWrite"]},
{"name": "automation_execute", "permission_type": ["NonData"]}
],
"identity_to_permissions": [
{
"identity": "sn-integration-user",
"identity_type": "local_user",
"application_permissions": [
{
"application": "ServiceNow - corp.service-now.com",
"resources": [
{"resource": "incident", "permission": "incident_read"},
{"resource": "incident", "permission": "incident_write"},
{"resource": "hr_case", "permission": "hr_case_read"},
{"resource": "hr_case", "permission": "hr_case_write"},
{"resource": "AzureGraphRouter", "permission": "automation_execute"}
]
}
]
}
]
}
}

Note: The Entra side would be a SEPARATE OAA Application:

{
"applications": [
{
"name": "Entra ID - tenant.onmicrosoft.com",
"application_type": "EntraID",
"local_users": [
{
"name": "sp-azure-graph-router",
"unique_id": "uuid-sp-azure-graph-router",
"identities": ["a1b2c3d4-...@tenant.onmicrosoft.com"],
"is_active": true,
"custom_properties": {
"identity_type": "service_principal",
"appId": "a1b2c3d4-...",
"servicePrincipalType": "Application",
"linked_servicenow_automation": "AzureGraphRouter",
"chain_id": "chain-d4e5f6a7b8c9"
}
}
],
"local_roles": [
{
"name": "User.Read.All",
"permissions": ["user_data_read"]
},
{
"name": "Group.ReadWrite.All",
"permissions": ["group_data_read", "group_data_write"]
}
],
"resources": [
{
"name": "Microsoft Graph API",
"resource_type": "api",
"custom_properties": {
"domain": "graph.microsoft.com",
"business_domain": "identity_platform",
"sensitivity": "confidential"
}
}
]
}
]
}

Document statistics (Approach B):

  • 2 Applications (ServiceNow + Entra)
  • 1 + 1 Local Users (split by system)
  • 2 + 2 Local Roles (split by system)
  • Hierarchical resources (4-level nesting in ServiceNow)
  • Cross-application linkage via custom_properties only

9.4 Approach C: SecurityV0 Internal Model (entities + execution_chains)

This is what SecurityV0 actually stores — the canonical representation:

entities collection (7 documents for this chain):

[
{
"_id": "uuid-br-azuregraphrouter",
"tenant_id": "tenant-1",
"entity_type": "identity",
"source_system": "servicenow",
"source_id": "br-sys-id-azuregraphrouter",
"properties": {
"identity_type": "business_rule",
"display_name": "AzureGraphRouter",
"execution_mode": "autonomous",
"security_relevance": "active_external",
"status": "active",
"table": "incident",
"trigger_condition": "insert",
"active": true,
"source_metadata": {
"sys_id": "br-sys-id-azuregraphrouter",
"name": "AzureGraphRouter",
"table": "incident",
"when": "before",
"active": true
},
"source_metadata_hash": "sha256:..."
},
"relationships": [
{
"type": "TRIGGERS_ON",
"target_id": "uuid-res-incident",
"properties": {"trigger_type": "event", "condition": "insert"}
},
{
"type": "RUNS_AS",
"target_id": "uuid-identity-sn-integration-user",
"properties": {"run_as_type": "configured"}
},
{
"type": "CREATED_BY",
"target_id": "uuid-owner-admin",
"properties": {"created_at": "2024-06-15"}
}
],
"execution_paths": [],
"sync_version": 42,
"last_synced_at": "2026-02-13T10:00:00Z"
},
{
"_id": "uuid-identity-si-azuregraphhelper",
"tenant_id": "tenant-1",
"entity_type": "identity",
"source_system": "servicenow",
"source_id": "si-sys-id-azuregraphhelper",
"properties": {
"identity_type": "business_rule",
"display_name": "ScriptInclude: AzureGraphHelper",
"execution_mode": "autonomous",
"security_relevance": "active_external",
"status": "active",
"api_name": "AzureGraphHelper"
},
"relationships": [
{
"type": "RUNS_AS",
"target_id": "uuid-identity-sn-integration-user",
"properties": {"run_as_type": "inherited"}
}
],
"sync_version": 42
},
{
"_id": "uuid-res-rest-msgraph",
"tenant_id": "tenant-1",
"entity_type": "resource",
"source_system": "servicenow",
"source_id": "rm-sys-id-msgraph",
"properties": {
"resource_type": "api_endpoint",
"resource_name": "MSGraphAPI",
"business_domain": "identity_platform",
"sensitivity": "confidential",
"endpoint": "https://graph.microsoft.com/v1.0"
},
"relationships": [],
"sync_version": 42
},
{
"_id": "uuid-cred-oauth-azuread",
"tenant_id": "tenant-1",
"entity_type": "credential",
"source_system": "servicenow",
"source_id": "oauth-sys-id-azuread",
"properties": {
"credential_type": "oauth_client_secret",
"display_name": "AzureADOAuth",
"status": "active",
"grant_type": "client_credentials",
"issuing_system": "servicenow",
"target_system": "entra_id",
"target_endpoint": "https://login.microsoftonline.com/tenant/oauth2/v2.0/token"
},
"relationships": [],
"sync_version": 42
},
{
"_id": "uuid-identity-sn-integration-user",
"tenant_id": "tenant-1",
"entity_type": "identity",
"source_system": "servicenow",
"source_id": "sys-user-sn-integration",
"properties": {
"identity_type": "machine_account",
"display_name": "sn-integration-user",
"status": "active"
},
"relationships": [
{
"type": "AUTHENTICATES_TO",
"target_id": "uuid-identity-sp-azure-graph-router",
"properties": {
"via_credential_id": "uuid-cred-oauth-azuread",
"auth_protocol": "oauth2",
"target_system": "entra_id",
"trust_chain_position": 0,
"evidence_references": {
"issuing_system_id": "a1b2c3d4-...",
"issuing_tenant_id": "72f988bf-...",
"target_system_id": "a1b2c3d4-...",
"target_instance_id": "https://corp.service-now.com",
"matching_field": "client_id",
"matching_value": "a1b2c3d4-..."
}
}
},
{
"type": "HAS_ROLE",
"target_id": "uuid-role-itil",
"properties": {"granted_at": "2024-06-15"}
},
{
"type": "HAS_ROLE",
"target_id": "uuid-role-hr-agent",
"properties": {"granted_at": "2025-03-15"}
}
],
"execution_paths": [
{
"resource_id": "uuid-res-incident",
"resource_name": "incident",
"business_domain": "it_ops",
"sensitivity": "internal",
"via_roles": ["itil"],
"actions": ["read", "update"],
"source_system": "servicenow",
"auth_chain_depth": 0,
"computed_at": "2026-02-13T10:00:00Z"
},
{
"resource_id": "uuid-res-hr-case",
"resource_name": "hr_case",
"business_domain": "hr",
"sensitivity": "confidential",
"via_roles": ["hr_agent_workspace"],
"actions": ["read", "update"],
"source_system": "servicenow",
"auth_chain_depth": 0,
"computed_at": "2026-02-13T10:00:00Z"
}
],
"sync_version": 42
},
{
"_id": "uuid-identity-sp-azure-graph-router",
"tenant_id": "tenant-1",
"entity_type": "identity",
"source_system": "entra_id",
"source_id": "sp-id-azure-graph-router",
"properties": {
"identity_type": "service_principal",
"display_name": "sp-azure-graph-router",
"status": "active",
"source_metadata": {
"appId": "a1b2c3d4-...",
"servicePrincipalType": "Application",
"signInAudience": "AzureADMyOrg"
},
"source_metadata_hash": "sha256:..."
},
"relationships": [
{
"type": "HAS_ROLE",
"target_id": "uuid-role-entra-user-read",
"properties": {"granted_at": "2024-06-15"}
},
{
"type": "HAS_ROLE",
"target_id": "uuid-role-entra-group-rw",
"properties": {"granted_at": "2024-06-15"}
}
],
"execution_paths": [
{
"resource_id": "uuid-res-graph-api",
"resource_name": "Microsoft Graph API",
"business_domain": "identity_platform",
"sensitivity": "confidential",
"via_roles": ["User.Read.All", "Group.ReadWrite.All"],
"actions": ["read", "update"],
"source_system": "entra_id",
"auth_chain_depth": 0,
"computed_at": "2026-02-13T10:00:00Z"
}
],
"sync_version": 42
},
{
"_id": "uuid-res-incident",
"tenant_id": "tenant-1",
"entity_type": "resource",
"source_system": "servicenow",
"source_id": "table-incident",
"properties": {
"resource_type": "table",
"resource_name": "incident",
"business_domain": "it_ops",
"sensitivity": "internal",
"contains_pii": false
},
"relationships": [],
"accessible_by": [
{
"identity_id": "uuid-identity-sn-integration-user",
"identity_name": "sn-integration-user",
"via_roles": ["itil"],
"actions": ["read", "update"],
"computed_at": "2026-02-13T10:00:00Z"
}
],
"sync_version": 42
}
]

execution_chains collection (1 document):

{
"_id": "chain-d4e5f6a7b8c9",
"tenant_id": "tenant-1",
"name": "AzureGraphRouter Incident Routing",
"anchor_entity_id": "uuid-br-azuregraphrouter",
"entity_refs": [
{"entity_id": "uuid-br-azuregraphrouter", "role": "entry_point", "entity_type": "identity", "source_system": "servicenow"},
{"entity_id": "uuid-identity-si-azuregraphhelper", "role": "code_executor", "entity_type": "identity", "source_system": "servicenow"},
{"entity_id": "uuid-res-rest-msgraph", "role": "outbound_endpoint", "entity_type": "resource", "source_system": "servicenow"},
{"entity_id": "uuid-cred-oauth-azuread", "role": "auth_credential", "entity_type": "credential", "source_system": "servicenow"},
{"entity_id": "uuid-identity-sn-integration-user", "role": "executor_identity", "entity_type": "identity", "source_system": "servicenow"},
{"entity_id": "uuid-identity-sp-azure-graph-router", "role": "destination_identity", "entity_type": "identity", "source_system": "entra_id"},
{"entity_id": "uuid-res-incident", "role": "trigger_resource", "entity_type": "resource", "source_system": "servicenow"}
],
"summary": {
"trigger": "incident table insert",
"destination": "graph.microsoft.com",
"egress_category": "external",
"blast_radius_domains": ["it_ops", "hr", "identity_platform"],
"ownership_status": "orphaned",
"total_roles": 4,
"max_sensitivity": "confidential",
"cross_system": true,
"source_systems": ["servicenow", "entra_id"],
"execution_mode": "autonomous",
"security_relevance": "active_external"
},
"composition_hash": "sha256:a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"first_detected_at": "2026-02-12T00:00:00Z",
"last_seen_at": "2026-02-13T10:00:00Z",
"sync_version": 42
}

9.5 Comparison Summary

DimensionApproach A (Automation-as-App)Approach B (Automation-as-Resource)Approach C (SecurityV0 Internal)
Documents1 Application, 6 Resources2 Applications, nested Resources7 entities, 1 execution_chain
Cross-systemForced into single appSeparate apps, custom_property linkAUTHENTICATES_TO edge
Chain structureFlat resource list + role hintsSub-resource hierarchyentity_refs with ordered roles
Execution directionLost (DataRead/DataWrite only)LostPreserved (edge types)
TemporalNot expressibleNot expressibleentity_versions + events
Permission accuracyBoth SPs are "local users"Correct per-systemCorrect per-system
OAA complianceViolates conventionsFollows conventionsNot OAA (internal model)

10. Recommendation

10.1 Architecture Decision

Use the Hybrid Model:

  1. Internal model (entities + execution_chains): Continues as designed. Automations are autonomous_identity nodes. Execution chains are composition documents in a separate collection. This is SecurityV0's canonical representation and serves all internal queries, findings, temporal analysis, and evidence packs.

  2. OAA export (Approach B): When exporting to Veza or other OAA-compatible tools, use the automation-as-resource model. Each source platform is one OAA Application. Automations become typed Resources within the hierarchy. Cross-system links use custom_properties.

  3. NormalizedNodeType: No changes to the enum. Add semantic richness through properties.resource_type subtype discrimination. The entity projection layer handles type mapping for OAA export.

  4. Neo4j portability: SecurityV0's internal model (Approach C) maps directly to Neo4j with multi-label nodes. ExecutionChain nodes reference their constituent entities via CONTAINS edges. This supports both entity-level and chain-level Cypher queries.

10.2 Why Not Full OAA Alignment?

OAA is a permission snapshot format. SecurityV0 is a temporal execution authority platform. Full alignment would mean:

  • Losing execution direction (trigger -> process -> output)
  • Losing temporal tracking (OAA is point-in-time)
  • Losing execution evidence (OAA has no concept)
  • Losing chain identity (OAA has no composition model)
  • Losing the dual-identity property of automations (they are BOTH actors and artifacts)

The correct relationship is: SecurityV0's richer model projects into OAA format for interoperability. OAA is an export format, not a storage model.

10.3 Answering the Founder's Question

"OAA has application notion, not only identity. So stuff like script include, business flow looks more like an application, but not identity."

The founder is right that these artifacts have application-like properties (they have configuration, sub-components, permissions). But they ALSO have identity-like properties (they execute, they run-as, they authenticate). OAA cannot represent this duality because OAA's world is strictly divided: Applications contain Resources, and Users (identities) have Permissions on Resources.

SecurityV0's insight is that automation artifacts are simultaneously identities AND resources. The internal model captures both dimensions by:

  • Modeling them as autonomous_identity (they execute)
  • Attaching resource-like properties via properties.* (they are configured)
  • Connecting them via RUNS_AS to the identity they execute as
  • Connecting them via TRIGGERS_ON to the resource that activates them

For OAA export, we project the resource dimension (losing the identity/execution dimension). For internal analysis, we use the full model.

"When there is an incident trigger - can it be 'data' or 'resource', and when updating some data later like incident table - it looks like a change to 'data' or 'resource' as well."

Correct. The incident table is a Resource in all three roles (trigger source, data input, data output). The difference is not what the table IS, but what RELATIONSHIP it has to the automation:

  • TRIGGERS_ON — the table as event source
  • EXECUTES_ON — the table as data access target
  • APPLIES_TO (via permission chain) — the table as permitted scope

OAA collapses all three into DataRead/DataWrite permissions. SecurityV0 distinguishes them via typed edges.

10.4 Implementation Priority

PriorityActionEffortDependency
P0Implement execution_chains collection (Round 3 Phase 1)94hNone
P1Add resource_type subtypes for automation components4hNone
P2Implement OAA export endpoint (GET /export/oaa/:sourceSystem)24hP0 + P1
P3Neo4j schema design and migration scripts16hP0
P4OAA export for Entra ID (second Application)8hP2

10.5 Files That Would Change

FileChangeReason
sv0-platform/src/ingestion/types.tsNo change to NormalizedNodeTypeType stability preserved
sv0-platform/src/storage/New execution_chains collection + indexesRound 3 Phase 1
sv0-platform/src/domain/graph/Chain builder serviceRound 3 Phase 1
sv0-platform/src/api/routes/New export route + chain routesOAA export + chain API
sv0-documentation/docs/architecture/03-database.mdAdd execution_chains schemaDocumentation
sv0-documentation/docs/architecture/01-data-model.mdAdd resource subtypes, chain entityDocumentation

Appendix A: OAA Permission Mapping Reference

SecurityV0 Normalized Actions -> OAA Canonical Permissions

SecurityV0 ActionOAA PermissionNotes
createDataCreate or DataWriteOAA has both; use DataCreate when available
readDataReadDirect mapping
updateDataWriteDirect mapping
deleteDataDeleteDirect mapping
executeNonDataOAA has no "execute" — closest is NonData
adminResourceAdmin or AccountAdminDepends on scope
delegateOwnershipAssignmentClosest match

Edge Type -> OAA Concept

SecurityV0 EdgeOAA EquivalentFidelity
OWNED_BYCustom property on local_userLossy (no ownership model)
HAS_ROLElocal_user.add_role()Direct
GRANTSrole permissions listDirect
APPLIES_TOpermission -> resource bindingDirect
AUTHENTICATES_TOIdP identity linkPartial (users only, not resources)
RUNS_ASNo equivalentLost in OAA
TRIGGERS_ONNo equivalentLost in OAA
EXECUTES_ONNo equivalentLost in OAA
CREATED_BYNo equivalentLost in OAA

Appendix B: Neo4j Label Mapping Reference

Entity Type -> Neo4j Labels

Entity Typeidentity_type/resource_typeNeo4j Labels
identityservice_principal:Entity:Identity:ServicePrincipal
identityoauth_app:Entity:Identity:OAuthApp
identitymachine_account:Entity:Identity:MachineAccount
identitybusiness_rule:Entity:Identity:Automation:BusinessRule
identityflow_designer_flow:Entity:Identity:Automation:FlowDesignerFlow
identityscheduled_job:Entity:Identity:Automation:ScheduledJob
owner (human)human:Entity:Owner:Human
owner (team)team:Entity:Owner:Team
role*:Entity:Role
permission*:Entity:Permission
resourcetable:Entity:Resource:Table
resourceapi_endpoint:Entity:Resource:APIEndpoint
credentialoauth_client_secret:Entity:Credential:OAuthSecret
execution_chain*:ExecutionChain

Relationship Type -> Neo4j Edge Type

All SecurityV0 relationship types map 1:1 to Neo4j edge types. Edge properties transfer directly.

// Example: Complete AzureGraphRouter chain in Neo4j
CREATE (br:Entity:Identity:Automation:BusinessRule {
entity_id: "uuid-br-azuregraphrouter",
name: "AzureGraphRouter",
identity_type: "business_rule",
execution_mode: "autonomous",
security_relevance: "active_external"
})

CREATE (si:Entity:Identity:Automation:BusinessRule {
entity_id: "uuid-si-azuregraphhelper",
name: "AzureGraphHelper",
identity_type: "business_rule"
})

CREATE (sn:Entity:Identity:MachineAccount {
entity_id: "uuid-sn-integration-user",
name: "sn-integration-user",
identity_type: "machine_account",
source_system: "servicenow"
})

CREATE (sp:Entity:Identity:ServicePrincipal {
entity_id: "uuid-sp-azure-graph-router",
name: "sp-azure-graph-router",
identity_type: "service_principal",
source_system: "entra_id"
})

CREATE (incident:Entity:Resource:Table {
entity_id: "uuid-res-incident",
name: "incident",
business_domain: "it_ops",
sensitivity: "internal"
})

CREATE (graph_api:Entity:Resource:APIEndpoint {
entity_id: "uuid-res-graph-api",
name: "Microsoft Graph API",
business_domain: "identity_platform",
sensitivity: "confidential"
})

CREATE (chain:ExecutionChain {
chain_id: "chain-d4e5f6a7b8c9",
name: "AzureGraphRouter Incident Routing",
composition_hash: "sha256:a1b2c3d4...",
egress_category: "external",
max_sensitivity: "confidential"
})

// Edges
CREATE (br)-[:TRIGGERS_ON {trigger_type: "event", condition: "insert"}]->(incident)
CREATE (br)-[:RUNS_AS {run_as_type: "configured"}]->(sn)
CREATE (sn)-[:AUTHENTICATES_TO {auth_protocol: "oauth2", trust_chain_position: 0}]->(sp)
CREATE (sp)-[:HAS_ROLE]->(role_user_read:Entity:Role {name: "User.Read.All"})
CREATE (sp)-[:HAS_ROLE]->(role_group_rw:Entity:Role {name: "Group.ReadWrite.All"})
CREATE (role_user_read)-[:GRANTS]->(perm_user_read:Entity:Permission {action: "read", scope: "User"})
CREATE (role_group_rw)-[:GRANTS]->(perm_group_rw:Entity:Permission {action: "update", scope: "Group"})
CREATE (perm_user_read)-[:APPLIES_TO]->(graph_api)
CREATE (perm_group_rw)-[:APPLIES_TO]->(graph_api)

// Chain membership
CREATE (chain)-[:ANCHORED_BY]->(br)
CREATE (chain)-[:CONTAINS {role: "entry_point", position: 0}]->(br)
CREATE (chain)-[:CONTAINS {role: "code_executor", position: 1}]->(si)
CREATE (chain)-[:CONTAINS {role: "executor_identity", position: 2}]->(sn)
CREATE (chain)-[:CONTAINS {role: "destination_identity", position: 3}]->(sp)
CREATE (chain)-[:CONTAINS {role: "trigger_resource", position: 4}]->(incident)

Appendix C: Decision Matrix

CriterionWeightApproach A (as App)Approach B (as Resource)Hybrid (Recommended)
OAA convention compliance31 (violates)5 (follows)4 (follows for export)
Execution semantics preserved51 (lost)2 (mostly lost)5 (fully preserved)
Cross-system chain expression52 (forced into one app)3 (separate apps + hints)5 (AUTHENTICATES_TO)
Temporal tracking51 (OAA has none)1 (OAA has none)5 (entity_versions + events)
Neo4j portability42 (separate graph)3 (needs hop)5 (direct traversal)
Implementation effort33 (new app-per-chain)4 (follows existing patterns)4 (export layer only)
Rollback safety22 (many apps to remove)4 (resource hierarchy)5 (export is stateless)
Query simplicity (CISO)42 (cross-app queries)3 (resource + hop)5 (single traversal)
Weighted Total5289148

The hybrid model scores nearly 3x Approach A and 1.7x Approach B on weighted criteria. The difference is driven by execution semantics, temporal tracking, and cross-system expression — the three capabilities that define SecurityV0's differentiation.


Round 4 analysis complete. OAA alignment is achievable as an export projection from SecurityV0's richer internal model. The execution_chains collection from Round 3 is complementary to (not replaced by) OAA alignment. The NormalizedNodeType enum should remain stable; semantic richness comes from property-level subtype discrimination.