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
- Executive Summary
- OAA Data Model Recap
- Approach A: Automation as OAA Application
- Approach B: Automation as OAA Resource
- The Trigger/Data Problem
- NormalizedNodeType Evolution
- Reconciling execution_chains with OAA
- Neo4j Portability
- Concrete AzureGraphRouter Mapping
- 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_chainscollection 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
| Permission | Category | Description |
|---|---|---|
| DataRead | Data | Read data records |
| DataWrite | Data | Create or modify data records |
| DataDelete | Data | Remove data records |
| DataCreate | Data | Create new data records |
| MetadataRead | Metadata | Read configuration/schema |
| MetadataWrite | Metadata | Modify configuration/schema |
| NonData | Non-data | Actions that do not touch data (e.g., login) |
| OwnershipAssignment | Admin | Assign ownership of resources |
| ResourceAdmin | Admin | Administer resource configuration |
| AccountAdmin | Admin | Administer user accounts |
2.3 OAA Patterns from Community Connectors
Studying the community connectors reveals consistent patterns:
| Connector | Application | Resources | Sub-Resources | Key Pattern |
|---|---|---|---|---|
| GitHub | Github - {org} | Repositories (repository) | None | Teams as groups, branch protection as properties |
| Jira | Jira - {instance} | Projects (project) | None | Permission schemes mapped per-project to roles |
| PagerDuty | PagerDuty - {subdomain} | Teams (team) | None | Global roles + team-scoped roles |
| Bitbucket | Bitbucket - {workspace} | Projects (Project) | Repos (Repo) | Hierarchical: Project > Repo |
| GitLab | GitLab - {instance} | Groups (group) | Projects (project) | Nested groups via sub-resources |
| Looker | Looker - {instance} | Model Sets | Models > Connections | 3-level hierarchy with DB connections |
Key observations:
- Each source platform becomes one OAA Application
- Resources represent data containers or organizational units, never executable logic
- Sub-resources model containment hierarchies (project > repo, group > project)
- Permissions flow from Identity -> Role -> Permission -> Resource
- 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
| Concept | OAA Support | SecurityV0 Need |
|---|---|---|
| Execution direction (input -> process -> output) | None | Critical |
| Temporal change tracking | None (snapshot-based) | Critical |
| Cross-system authentication chains | Partial (IdP linkage) | Critical |
| Automation as executable entity | None | Critical |
| Execution evidence (proof of action) | None | Critical |
| Ownership decay / orphaned NHI | None | Critical |
| Composition (chain of artifacts) | None | Critical |
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 Concept | NormalizedGraph Mapping | Notes |
|---|---|---|
| Application | No direct mapping — would need new NormalizedNodeType: "application" | The chain itself has no current node type |
| Local User | autonomous_identity nodes | SP and integration user are identities |
| Local Role | role nodes | Maps directly |
| Permission | permission nodes | Maps directly |
| Resource | resource nodes | Some new: trigger_source, script_component, outbound_endpoint |
| Custom Properties | NormalizedNode.properties | Arbitrary key-value |
| Identity linkage | AUTHENTICATES_TO edges | OAA 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 Type | SecurityV0 Entity Type | NormalizedNodeType | Relationship |
|---|---|---|---|
business_rule | Identity (identity_type: business_rule) | autonomous_identity | The same entity, different representation |
script_include | Identity or Resource (depends on context) | autonomous_identity or resource | Script includes are callable code |
rest_message | Resource (outbound endpoint) | resource | Configuration artifact |
oauth_profile | Credential | credential | Authentication material |
table | Resource | resource | Data container |
flow_designer_flow | Identity (identity_type: flow_designer_flow) | autonomous_identity | Executable 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
| Factor | Approach A (as App) | Approach B (as Resource) |
|---|---|---|
| OAA convention | Violates (1 app per platform) | Follows (1 app per platform) |
| Resource duplication | incident table in 12 apps | incident table once, shared |
| Scalability | 50 BRs = 50 applications | 50 BRs = 50 sub-resources |
| Cross-system auth | Forced into local_users | External IdP link (correct) |
| Hierarchy expression | Flat resources list | Nested sub-resources |
| Community pattern | No precedent | Matches 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:
| Dimension | OAA Permission | SecurityV0 Execution Chain |
|---|---|---|
| Direction | Bidirectional capability | Directed flow (input -> process -> output) |
| Trigger | Not modeled | Explicit (event, schedule, API call) |
| Causality | "Can do" | "Does do because of" |
| Composition | Flat (identity -> resource) | Ordered (trigger -> executor -> target) |
| Time | Snapshot | Temporal (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 Permission | Meaning | Example |
|---|---|---|
EventTrigger | Resource emits an event that starts this process | incident table -> BR |
DataRead_AsInput | Data is read as input to processing | BR reads incident fields |
DataWrite_AsOutput | Data is written as output of processing | BR writes to Graph API |
ExecuteLogic | Resource contains executable logic that runs | ScriptInclude code runs |
InvokeEndpoint | Outbound call to external system | REST 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:
-
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.
-
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.
-
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->identityhuman_identity->ownerrole->rolepermission->permissionresource->resourcecredential->credentialexecution_evidence-> stored inexecution_evidencecollection
6.2 OAA Type Mapping
| OAA Type | Current SecurityV0 Type | Alignment |
|---|---|---|
| Application | No equivalent | Gap |
| Local User | autonomous_identity | Partial (OAA user != NHI) |
| Local Group | No equivalent (implicit in roles) | Gap |
| Local Role | role | Direct |
| Permission | permission | Direct |
| Resource | resource | Direct |
| Sub-Resource | resource (flat) | Needs hierarchy |
| IdP User | human_identity or autonomous_identity | Depends 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:
| Entity | SecurityV0 Internal | OAA Export | Neo4j Export |
|---|---|---|---|
| Business Rule | autonomous_identity | resource (business_rule) | :Identity:Automation:BusinessRule |
| Script Include | autonomous_identity or resource | resource (script_include) | :Resource:ScriptComponent |
| REST Message | resource | resource (rest_message) | :Resource:OutboundEndpoint |
| OAuth Profile | credential | resource (auth_config) | :Credential:OAuthProfile |
| Service Principal | autonomous_identity | local_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
| Capability | execution_chains Collection | OAA-Aligned Entity Model |
|---|---|---|
| Stable chain identity | Yes (chain_id from anchor) | No (OAA has no chain concept) |
| Chain listing / search | Yes (direct collection query) | No |
| Chain-level temporal diff | Yes (composition_hash comparison) | No (OAA is snapshot-only) |
| Chain-level findings | Yes (orphaned chain, blast radius expansion) | No |
| Permission export to Veza | No | Yes |
| Standard-based interop | No | Yes (OAA JSON format) |
| Resource hierarchy | No (references entity IDs) | Yes (sub-resource nesting) |
| Identity-to-resource permission mapping | No | Yes |
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:
| Field | Source of Truth | OAA Representation |
|---|---|---|
| Entity identity (sys_id, etc.) | entities collection | Resource unique_id |
| Entity relationships | entities.relationships[] | Sub-resource hierarchy |
| Chain composition | execution_chains.entity_refs[] | Custom property on anchor Resource |
| Chain summary (egress, blast radius) | execution_chains.summary | Custom properties |
| Permissions | entities (role/permission nodes) | OAA permissions + identity_to_permissions |
| Execution evidence | execution_evidence collection | Not exported to OAA |
| Temporal history | entity_versions + events | Not 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.
8.3 Approach C: Hybrid — SecurityV0's Current Model in Neo4j (Recommended)
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
| Factor | Approach A (App) | Approach B (Resource) | Approach C (Hybrid) |
|---|---|---|---|
| Automation in permission traversal | No (separate graph) | Via RUNS_AS hop | Direct (is Identity) |
| CISO query complexity | 2+ patterns | 2 patterns | 1 pattern |
| Chain query | Match Application nodes | Match Resource nodes + hop | Match Chain + CONTAINS |
| Multi-label support | :Application | :Resource | :Identity:Automation:BusinessRule |
| Temporal integration | Separate layer | Separate layer | Edge properties + events |
| Cross-system traversal | Across Application boundaries | AUTHENTICATES_TO between apps | Direct AUTHENTICATES_TO |
| Reverse query ("what automations access HR?") | Per-app search | Resource search + RUNS_AS hop | Direct traversal |
8.5 Neo4j Schema for the Recommended Hybrid
// 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
| Dimension | Approach A (Automation-as-App) | Approach B (Automation-as-Resource) | Approach C (SecurityV0 Internal) |
|---|---|---|---|
| Documents | 1 Application, 6 Resources | 2 Applications, nested Resources | 7 entities, 1 execution_chain |
| Cross-system | Forced into single app | Separate apps, custom_property link | AUTHENTICATES_TO edge |
| Chain structure | Flat resource list + role hints | Sub-resource hierarchy | entity_refs with ordered roles |
| Execution direction | Lost (DataRead/DataWrite only) | Lost | Preserved (edge types) |
| Temporal | Not expressible | Not expressible | entity_versions + events |
| Permission accuracy | Both SPs are "local users" | Correct per-system | Correct per-system |
| OAA compliance | Violates conventions | Follows conventions | Not OAA (internal model) |
10. Recommendation
10.1 Architecture Decision
Use the Hybrid Model:
-
Internal model (entities + execution_chains): Continues as designed. Automations are
autonomous_identitynodes. 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. -
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.
-
NormalizedNodeType: No changes to the enum. Add semantic richness through
properties.resource_typesubtype discrimination. The entity projection layer handles type mapping for OAA export. -
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_ASto the identity they execute as - Connecting them via
TRIGGERS_ONto 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 sourceEXECUTES_ON— the table as data access targetAPPLIES_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
| Priority | Action | Effort | Dependency |
|---|---|---|---|
| P0 | Implement execution_chains collection (Round 3 Phase 1) | 94h | None |
| P1 | Add resource_type subtypes for automation components | 4h | None |
| P2 | Implement OAA export endpoint (GET /export/oaa/:sourceSystem) | 24h | P0 + P1 |
| P3 | Neo4j schema design and migration scripts | 16h | P0 |
| P4 | OAA export for Entra ID (second Application) | 8h | P2 |
10.5 Files That Would Change
| File | Change | Reason |
|---|---|---|
sv0-platform/src/ingestion/types.ts | No change to NormalizedNodeType | Type stability preserved |
sv0-platform/src/storage/ | New execution_chains collection + indexes | Round 3 Phase 1 |
sv0-platform/src/domain/graph/ | Chain builder service | Round 3 Phase 1 |
sv0-platform/src/api/routes/ | New export route + chain routes | OAA export + chain API |
sv0-documentation/docs/architecture/03-database.md | Add execution_chains schema | Documentation |
sv0-documentation/docs/architecture/01-data-model.md | Add resource subtypes, chain entity | Documentation |
Appendix A: OAA Permission Mapping Reference
SecurityV0 Normalized Actions -> OAA Canonical Permissions
| SecurityV0 Action | OAA Permission | Notes |
|---|---|---|
create | DataCreate or DataWrite | OAA has both; use DataCreate when available |
read | DataRead | Direct mapping |
update | DataWrite | Direct mapping |
delete | DataDelete | Direct mapping |
execute | NonData | OAA has no "execute" — closest is NonData |
admin | ResourceAdmin or AccountAdmin | Depends on scope |
delegate | OwnershipAssignment | Closest match |
Edge Type -> OAA Concept
| SecurityV0 Edge | OAA Equivalent | Fidelity |
|---|---|---|
OWNED_BY | Custom property on local_user | Lossy (no ownership model) |
HAS_ROLE | local_user.add_role() | Direct |
GRANTS | role permissions list | Direct |
APPLIES_TO | permission -> resource binding | Direct |
AUTHENTICATES_TO | IdP identity link | Partial (users only, not resources) |
RUNS_AS | No equivalent | Lost in OAA |
TRIGGERS_ON | No equivalent | Lost in OAA |
EXECUTES_ON | No equivalent | Lost in OAA |
CREATED_BY | No equivalent | Lost in OAA |
Appendix B: Neo4j Label Mapping Reference
Entity Type -> Neo4j Labels
| Entity Type | identity_type/resource_type | Neo4j Labels |
|---|---|---|
| identity | service_principal | :Entity:Identity:ServicePrincipal |
| identity | oauth_app | :Entity:Identity:OAuthApp |
| identity | machine_account | :Entity:Identity:MachineAccount |
| identity | business_rule | :Entity:Identity:Automation:BusinessRule |
| identity | flow_designer_flow | :Entity:Identity:Automation:FlowDesignerFlow |
| identity | scheduled_job | :Entity:Identity:Automation:ScheduledJob |
| owner (human) | human | :Entity:Owner:Human |
| owner (team) | team | :Entity:Owner:Team |
| role | * | :Entity:Role |
| permission | * | :Entity:Permission |
| resource | table | :Entity:Resource:Table |
| resource | api_endpoint | :Entity:Resource:APIEndpoint |
| credential | oauth_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
| Criterion | Weight | Approach A (as App) | Approach B (as Resource) | Hybrid (Recommended) |
|---|---|---|---|---|
| OAA convention compliance | 3 | 1 (violates) | 5 (follows) | 4 (follows for export) |
| Execution semantics preserved | 5 | 1 (lost) | 2 (mostly lost) | 5 (fully preserved) |
| Cross-system chain expression | 5 | 2 (forced into one app) | 3 (separate apps + hints) | 5 (AUTHENTICATES_TO) |
| Temporal tracking | 5 | 1 (OAA has none) | 1 (OAA has none) | 5 (entity_versions + events) |
| Neo4j portability | 4 | 2 (separate graph) | 3 (needs hop) | 5 (direct traversal) |
| Implementation effort | 3 | 3 (new app-per-chain) | 4 (follows existing patterns) | 4 (export layer only) |
| Rollback safety | 2 | 2 (many apps to remove) | 4 (resource hierarchy) | 5 (export is stateless) |
| Query simplicity (CISO) | 4 | 2 (cross-app queries) | 3 (resource + hop) | 5 (single traversal) |
| Weighted Total | 52 | 89 | 148 |
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.