Skip to main content

OAA Mapping Analysis — Integrator

Date: 2026-02-13 (Round 4) Author: Integration Engineer (connector architecture, cross-system data flow, OAA integration) Status: Draft for team review Related: Round 3 automation persistence analysis, connector interface contract


Executive Summary

The founder raises a critical question: Should automation chains be modeled as OAA Applications, Resources, or some hybrid? This fundamentally affects connector architecture and cross-platform compatibility.

Core finding: OAA's Application model is incompatible with SecurityV0's execution chain concept. OAA Applications are isolated containers (each app has its own local_users, local_groups, resources). SecurityV0 execution chains are cross-system subgraphs that span multiple source platforms (ServiceNow → Entra ID → Graph API).

Recommended approach:

  1. Keep NormalizedGraph as the primary connector output — proven, flexible, supports cross-system stitching
  2. Build an OAA export adapter at the platform level — convert SecurityV0's graph to OAA format for Veza integration (if needed)
  3. Do NOT make connectors OAA-native — this breaks cross-system correlation and couples connectors to Veza's data model

Why this matters: Forcing automation chains into OAA's Application model would require either:

  • Creating one Application per source system (ServiceNow as an app, Entra as a separate app) and losing the cross-system execution chain in the boundary
  • Creating one Application per chain (impossible — connectors don't know chain boundaries)
  • Building a custom OAA schema extension (defeats the purpose of using a standard)

The right abstraction is: NormalizedGraph for connectors, OAA adapter for Veza export.


1. Connector Output Transformation

Current State (NormalizedGraph)

The Entra-ServiceNow connector outputs NormalizedGraph (nodes + edges). This is database-agnostic, platform-agnostic, and proven.

interface NormalizedGraph {
syncId: string;
connectorId: string;
tenantId: string;
transformedAt: DateTime;
nodes: NormalizedNode[];
edges: NormalizedEdge[];
temporalMarkers: TemporalMarker[];
evidenceCompleteness: EvidenceCompletenessReport;
}

Connector workflow:

Source APIs → RawExtraction → Transformer → NormalizedGraph → Platform Ingestion

Key properties:

  • Cross-system stitching happens at platform level via AUTHENTICATES_TO edges
  • Connectors emit source-system facts (nodes + edges), platform assembles chains
  • No dependency on downstream storage (MongoDB, Neo4j, OAA all consume the same graph)

OAA Model Structure

OAA defines Applications as isolated containers:

from oaaclient.templates import CustomApplication

app = CustomApplication(name="ServiceNow Production", application_type="servicenow")

# Application contains LOCAL entities
app.add_local_user("sn-integration-user")
app.add_local_group("admins")
app.add_local_role("itil")

# Resources are WITHIN the application
resource = app.add_resource("incident_table", resource_type="table")
sub_resource = resource.add_sub_resource("hr_case", resource_type="table")

# Permissions tie local users to resources
app.add_custom_permission("admin", [OAAPermission.DataRead, OAAPermission.DataWrite])
user = app.local_users["sn-integration-user"]
user.add_permission("admin", resources=[resource])

OAA connector workflow:

Source APIs → OAA Application Object → JSON Payload → POST to Veza API

Critical limitation: Each OAA Application is a self-contained authorization domain. Cross-application relationships (like Entra SP → ServiceNow user) are handled by IdP correlation (matching email addresses), not by explicit cross-system auth edges.

Mapping Problem: NormalizedNode → OAA Entity

NormalizedNode TypeOAA Entity TypeNotes
autonomous_identity (service_principal)local_userOAA doesn't distinguish autonomous from human — both are "users"
autonomous_identity (business_rule)local_user or CustomResource?Ambiguous — is a BR an identity or an executable resource?
human_identitylocal_user or external IdP referenceIf email exists, Veza auto-links to IdP users
rolelocal_roleDirect mapping
permissioncustom_permissionMust normalize to OAA's 10 canonical types
resourceCustomResourceDirect mapping
credentialNo OAA equivalentOAA has no first-class credential entity
execution_evidenceNo OAA equivalentOAA doesn't track execution logs

Fatal issue: OAA has no representation for:

  • AUTHENTICATES_TO edges (cross-system auth chains)
  • RUNS_AS edges (automation identity binding)
  • TRIGGERS_ON edges (automation triggers)
  • Credential entities (OAuth secrets, certs)
  • Execution evidence (sign-in logs, transaction logs)

Conclusion: NormalizedGraph → OAA is a lossy transformation. If we emit OAA-native payloads from connectors, we lose the execution path semantics that SecurityV0 depends on.


2. OAA SDK Integration

Three Integration Options

Option A: Dual-Output Connector

Connectors emit both NormalizedGraph AND OAA payload.

# In transformer.py
class EntraServiceNowTransformer:
def transform(self, discovered: DiscoveredEntities) -> dict:
# Build NormalizedGraph (existing)
graph = self._build_normalized_graph(discovered)

# Build OAA payload (new)
oaa_payload = self._build_oaa_payload(discovered)

return {
"normalized_graph": graph,
"oaa_payload": oaa_payload
}

def _build_oaa_payload(self, discovered: DiscoveredEntities) -> dict:
from oaaclient.templates import CustomApplication, OAAPermission

# ServiceNow as an Application
sn_app = CustomApplication("ServiceNow Production", application_type="servicenow")

# Entra ID as a separate Application
entra_app = CustomApplication("Entra ID", application_type="entra_id")

# Map entities to OAA local users/resources
for user in discovered.sn_users:
sn_app.add_local_user(user['user_name'], identities=[user.get('email')])

for sp in discovered.azure_sps:
entra_app.add_local_user(sp['displayName'], identities=[sp['appId']])

# Problem: How to represent the OAuth → SP authentication chain?
# OAA has no cross-application auth edge concept

return {
"applications": [sn_app.to_dict(), entra_app.to_dict()]
}

Pros:

  • Maintains NormalizedGraph for platform use
  • Provides OAA payload for direct Veza submission (if needed)

Cons:

  • Double the work (two formats, two schemas, two validation paths)
  • Cross-system chains lost in OAA representation (split into two isolated Applications)
  • Connector complexity doubles
  • Maintenance burden: schema changes require updates to both formats

Effort estimate: 8-10 days per connector (initial), +30% ongoing maintenance


Option B: Platform-Level OAA Adapter

Connectors emit only NormalizedGraph. Platform provides an OAA export API.

// sv0-platform/src/export/oaa-adapter.ts
class OAAExporter {
/**
* Convert SecurityV0 graph to OAA Application format.
* Maps each source system to an OAA Application.
*/
async exportToOAA(tenantId: string): Promise<OAAPayload> {
const entities = await this.storage.getEntities({ tenant_id: tenantId });

// Group by source system
const serviceNowEntities = entities.filter(e => e.source_system === 'servicenow');
const entraEntities = entities.filter(e => e.source_system === 'entra_id');

// Build OAA Applications
const snApp = this._buildOAAApplication('ServiceNow Production', serviceNowEntities);
const entraApp = this._buildOAAApplication('Entra ID', entraEntities);

return {
applications: [snApp, entraApp],
// Loss: cross-system AUTHENTICATES_TO edges not representable
};
}

private _buildOAAApplication(name: string, entities: Entity[]): CustomApplication {
// Map NormalizedGraph entities to OAA local users/roles/resources
// Note: Automation chains (execution_chains collection) not included
// because OAA has no multi-application subgraph concept
}
}
# Alternative: Platform HTTP endpoint
# POST /api/v1/tenants/:id/export/oaa
# Returns OAA JSON payload

import requests
response = requests.post(
"https://sv0-platform.example.com/api/v1/tenants/default/export/oaa",
headers={"X-Api-Key": api_key}
)
oaa_payload = response.json()

# Submit to Veza
veza_client = OAAClient(veza_url, veza_api_key)
for app in oaa_payload["applications"]:
veza_client.push_application(provider_name="SecurityV0", data_source_name=app["name"], application_object=app)

Pros:

  • Zero connector changes — connectors remain focused on NormalizedGraph
  • Platform owns the OAA transformation (single implementation, easier to maintain)
  • Cross-system chains CAN be partially represented (see hybrid approach below)
  • OAA becomes an optional export format, not a required connector output

Cons:

  • Requires platform export API (new feature)
  • OAA representation is still lossy (execution chains, credentials, evidence)
  • Veza integration is a separate step (not built into connector sync)

Effort estimate: 5-7 days (platform export API), 0 days per connector


Replace NormalizedGraph with OAA as the primary connector output.

# Bad idea: rewrite transformer to emit only OAA
class EntraServiceNowTransformer:
def transform(self, discovered: DiscoveredEntities) -> OAAPayload:
app = CustomApplication("Combined Integration", application_type="custom")

# All entities go into one application
# Loss: source system boundaries
# Loss: AUTHENTICATES_TO semantics
# Loss: execution evidence

return app.to_dict()

Pros:

  • None (for SecurityV0 use case)

Cons:

  • Breaks cross-system correlation — platform can't stitch Entra → ServiceNow without explicit edges
  • Loses execution path semantics — OAA has no AUTHENTICATES_TO, RUNS_AS, TRIGGERS_ON concepts
  • Couples connectors to Veza — if we switch from Veza or add Neo4j, connectors need rewrite
  • Loses temporal tracking — OAA snapshots, no diff engine integration
  • Loses evidence provenance — OAA has no execution_evidence or credential entities

Effort estimate: 10-12 days per connector, 100% of platform logic must change

Verdict: Do not pursue.


3. Resource Hierarchy Design

OAA Resource Model

OAA supports 2-level resource hierarchies:

# Level 1: Resource
parent = app.add_resource("ServiceNow Tables", resource_type="collection")

# Level 2: Sub-resource
child = parent.add_sub_resource("incident", resource_type="table")

# Level 3: NOT SUPPORTED (no sub-sub-resources in OAA SDK)
# This FAILS:
# grandchild = child.add_sub_resource("priority_field", resource_type="field")

Real-world OAA connector examples:

ConnectorResource Hierarchy
Lookermodel_set (L1) → model (L2) → connection (L3) — but L3 is flattened
GitLabgroup (L1) → project (L2)
BitbucketProject (L1) → Repo (L2)
GitHubrepository (L1 only, no sub-resources)

Looker workaround (from oaa_looker.py):

# They wanted: model_set → model → connection (3 levels)
# OAA limitation: only 2 levels
# Solution: Make connection a sibling sub-resource under model_set

model_set = app.add_resource("Analytics", resource_type="model_set")
model = model_set.add_sub_resource("sales_model", resource_type="model")
connection = model.add_sub_resource("snowflake_prod", resource_type="connection")
# connection is really model → connection, but stored as model_set child

Applying to Automation Chains

Scenario: AzureGraphRouter automation chain

ServiceNow: Business Rule → Script Include → REST Message → OAuth Entity
Entra ID: Service Principal → Roles

Attempt 1: Flatten into ServiceNow Application

sn_app = CustomApplication("ServiceNow Production", application_type="servicenow")

# Business Rule as a resource?
br_resource = sn_app.add_resource("AzureGraphRouter-BR", resource_type="business_rule")
si_resource = br_resource.add_sub_resource("AzureGraphRouter-SI", resource_type="script_include")
rest_resource = br_resource.add_sub_resource("Graph-Router", resource_type="rest_message")
# OAuth Entity — wait, this authenticates to Entra ID (different app)
# Can't represent cross-app relationship in OAA

Problem: OAA Resources are scoped to their Application. ServiceNow resources can't reference Entra ID entities.

Attempt 2: Use local_user for automation identities

sn_app = CustomApplication("ServiceNow Production", application_type="servicenow")

# Business Rule as a local user (autonomous identity)
br_user = sn_app.add_local_user("AzureGraphRouter-BR")

# Permissions
sn_app.add_custom_permission("execute", [OAAPermission.NonData])
br_user.add_permission("execute", resources=[incident_table])

# Problem: How to represent "BR CALLS SI CALLS REST Message"?
# OAA has no "calls" relationship between users
# OAA has no RUNS_AS concept

Attempt 3: Automation as a Custom Resource with metadata

sn_app = CustomApplication("ServiceNow Production", application_type="servicenow")

# Define automations as a resource type
automation_collection = sn_app.add_resource("Automations", resource_type="collection")
azure_graph_automation = automation_collection.add_sub_resource(
"AzureGraphRouter",
resource_type="automation_chain"
)

# Custom properties
azure_graph_automation.set_property("entry_point", "business_rule:AzureGraphRouter")
azure_graph_automation.set_property("destination", "graph.microsoft.com")
azure_graph_automation.set_property("cross_system", True)

# But now: how do we show who has access to this automation?
# OAA expects identities → resources
# An automation is both an identity (executes) and a resource (is managed)

Conclusion: OAA's resource model is incompatible with automation chains. Automations are not simple resources — they are composite entities that:

  • Execute as identities (RUNS_AS)
  • Call other resources (EXECUTES_ON)
  • Span system boundaries (AUTHENTICATES_TO)
  • Have temporal lifecycle (trigger schedules, execution evidence)

None of this maps to OAA's CustomResource or local_user abstractions.


4. Application-per-Chain Design

Founder's suggestion: "Stuff like script include, business flow looks more like an application, but not identity."

Interpretation: Each automation chain could be its own OAA Application.

# One application per automation chain
azure_graph_app = CustomApplication("AzureGraphRouter Automation", application_type="automation_chain")

# Chain members as local users?
br_user = azure_graph_app.add_local_user("BusinessRule-AzureGraphRouter")
si_user = azure_graph_app.add_local_user("ScriptInclude-AzureGraphRouter")
sp_user = azure_graph_app.add_local_user("ServicePrincipal-GraphApp")

# Resources accessed by the chain
incident_table = azure_graph_app.add_resource("incident", resource_type="table")
graph_api = azure_graph_app.add_resource("graph.microsoft.com/users", resource_type="api_endpoint")

# Permissions
azure_graph_app.add_custom_permission("read", [OAAPermission.DataRead])
sp_user.add_permission("read", resources=[graph_api])

Fatal Problem: Connectors Don't Know Chain Boundaries

Round 3 consensus: Chains are assembled by the platform, not discovered by connectors.

Why:

  • ServiceNow connector runs Monday, Entra connector runs Tuesday (partial graphs)
  • Chains span multiple connectors (ServiceNow + Entra + GitHub future)
  • Chain stability (credential rotation) requires temporal view (only platform has this)

Implication: If each chain is an OAA Application, connectors can't emit them. Only the platform can build them after cross-system stitching.

But: OAA Applications are submitted per-connector. The OAA model doesn't have a "platform assembles Applications from multiple connectors" pattern. Each connector pushes its own Applications to Veza.

Dead end: Application-per-chain design doesn't work unless we abandon the connector interface pattern entirely and make the platform the sole OAA submitter.


5. Cross-System Challenge: Execution Paths

The AzureGraphRouter Example

SecurityV0 representation (NormalizedGraph + AUTHENTICATES_TO edges):

// ServiceNow entities
{
"nodeId": "sn-br-abc",
"nodeType": "autonomous_identity",
"properties": { "identitySubtype": "business_rule" }
}
{
"nodeId": "sn-oauth-xyz",
"nodeType": "credential",
"properties": {
"credentialType": "oauth_client_secret",
"clientId": "a1b2c3d4-..."
}
}

// Entra ID entities
{
"nodeId": "entra-sp-123",
"nodeType": "autonomous_identity",
"properties": {
"identitySubtype": "service_principal",
"appId": "a1b2c3d4-..."
}
}

// Cross-system edge (created by platform)
{
"edgeId": "AUTH-sn-oauth-xyz-entra-sp-123",
"edgeType": "AUTHENTICATES_TO",
"sourceNodeId": "entra-sp-123",
"targetNodeId": "sn-oauth-xyz",
"properties": {
"evidenceReferences": {
"matchingField": "client_id",
"matchingValue": "a1b2c3d4-...",
"issuingSystemId": "a1b2c3d4-...",
"targetSystemId": "a1b2c3d4-..."
}
}
}

Path query (platform):

// Start: ServiceNow Business Rule
// End: Entra roles and permissions
const path = await pathMaterializer.computeExecutionPath(
"sn-br-abc",
{ followAuthChain: true }
);

// Result: BR → SI → REST → OAuth -[AUTHENTICATES_TO]-> SP → Role → Permission → Resource

OAA representation (two isolated Applications):

# ServiceNow Application
sn_app = CustomApplication("ServiceNow Production", application_type="servicenow")
sn_user = sn_app.add_local_user("sn-integration-user", identities=["sn-integration-user@corp.com"])
# ... roles, resources for ServiceNow side

# Entra ID Application
entra_app = CustomApplication("Entra ID", application_type="entra_id")
sp_user = entra_app.add_local_user("sp-hr-onboarding", identities=["a1b2c3d4-...@apps.onmicrosoft.com"])
# ... roles, resources for Entra side

# Problem: No way to represent "sn-integration-user authenticates AS sp-hr-onboarding"
# OAA cross-app relationships are identity correlation ONLY (email matching)

Veza's approach: IdP correlation.

If sn-integration-user@corp.com and a1b2c3d4-...@apps.onmicrosoft.com resolve to the same Okta user, Veza infers they're the same identity. But OAuth apps don't have human email addresses — they're service principals. The correlation breaks.

How do OAA community connectors handle cross-system auth?

GitHub connector (oaa_github.py):

  • GitHub is a single system
  • No cross-system auth (GitHub users access GitHub repos)
  • Not applicable

GitLab connector (oaa_gitlab.py):

  • GitLab is a single system
  • Personal Access Tokens (PATs) are modeled as properties on users, not separate credential entities
  • No cross-system scenarios

Looker connector (oaa_looker.py):

  • Looker models connect to databases (Snowflake, Redshift)
  • Database connections are modeled as sub-resources under models
  • BUT: The actual database (Snowflake) is a separate OAA Application submitted by a different connector
  • Cross-system relationship (Looker → Snowflake) is NOT represented
  • Veza displays them as separate authorization domains

PagerDuty connector (oaa_pagerduty.py):

  • PagerDuty users may have integrations with AWS, ServiceNow
  • Integrations are modeled as resources within PagerDuty Application
  • The target systems (AWS, ServiceNow) are separate Applications
  • No cross-app execution path

Pattern observed: OAA community connectors avoid cross-system authorization chains. Each connector submits a self-contained Application. If two systems interact (Looker → Snowflake, PagerDuty → AWS), that's shown as:

  • App A: Looker users can access model X
  • App B: Snowflake users can access table Y
  • NO CONNECTION between model X and table Y in the graph

Conclusion: OAA is designed for per-system authorization, not cross-system execution paths. SecurityV0's differentiator (deterministic execution chains across platforms) is not expressible in standard OAA.


6. Compatibility Matrix

Comparison: Three Approaches

DimensionA: Dual-Output (NormalizedGraph + OAA)B: Platform OAA AdapterC: OAA-Native Connectors
Cross-system auth chainsPartial (OAA payload loses chains)Partial (OAA export loses chains)Not supported
Execution evidenceNormalizedGraph: Yes, OAA: NoNormalizedGraph: Yes, OAA: NoNot supported
Credential trackingNormalizedGraph: Yes, OAA: NoNormalizedGraph: Yes, OAA: NoNot supported
Automation chainsNormalizedGraph: Yes (platform assembles), OAA: NoNormalizedGraph: Yes, OAA: NoNot supported
Temporal diffNormalizedGraph: Yes, OAA: Snapshot onlyNormalizedGraph: Yes, OAA: Snapshot onlySnapshot only
Veza compatibilityYes (OAA payload)Yes (OAA export API)Yes
Neo4j compatibilityYes (NormalizedGraph)Yes (NormalizedGraph)No (would need separate connector output)
Connector complexityHigh (dual schemas)Low (unchanged)Medium (OAA SDK integration)
Platform changesNoneExport API (+5-7 days)Major (rewrite ingestion, diff, storage)
Effort per connector8-10 days initial, +30% maintenance0 days10-12 days per connector
Rollback riskLow (NormalizedGraph unaffected)Negligible (export is additive)High (full rewrite)
Standards complianceOAA compliant (with limitations)OAA compliant (with limitations)OAA compliant (with limitations)

NormalizedGraph vs. OAA Feature Comparison

FeatureNormalizedGraphOAA
Identity typesautonomous_identity, human_identity (distinct)local_user (all identities)
Automation subtypesbusiness_rule, flow_designer_flow, scheduled_job, system_executionMust map to local_user or CustomResource (ambiguous)
CredentialsFirst-class credential entity with lifecycleNo credential entity
Cross-system authAUTHENTICATES_TO edge with evidence_referencesIdP correlation only (email matching)
Execution evidenceexecution_evidence entity, linked to identitiesNo execution tracking
Automation relationshipsRUNS_AS, TRIGGERS_ON, CREATED_BY edgesNo equivalents
OwnershipOWNED_BY edges with ownership levels (primary/secondary/inherited)local_user.identities for IdP linking (not ownership)
Temporal trackingTemporalMarker, diff engine integrationSnapshot only (full replace on each sync)
Resource hierarchyUnlimited depth (edges)2 levels max (resource → sub-resource)
Permission normalization7 canonical actions + scope10 canonical permissions (DataRead, DataWrite, ...)
Evidence completenessEvidenceCompletenessReport with per-source availabilityNo concept
Source metadataAllowlisted fields + hashFull API response (stored in Veza)

Verdict: NormalizedGraph is a superset of OAA. Transforming NormalizedGraph → OAA is lossy but possible. OAA → NormalizedGraph is not reversible.


7. Connector Transformation Code Examples

No connector changes. Platform exports OAA on demand.

// sv0-platform/src/export/oaa-exporter.ts
import { OAAPayload, OAAApplication, OAALocalUser, OAAResource } from './oaa-types';

class OAAExporter {
constructor(private storage: StorageAdapter) {}

async exportTenant(tenantId: string): Promise<OAAPayload> {
const entities = await this.storage.getEntities({ tenant_id: tenantId });
const relationships = await this.storage.getRelationships({ tenant_id: tenantId });

// Group by source system
const systemGroups = this._groupBySourceSystem(entities);

const applications: OAAApplication[] = [];

for (const [sourceSystem, systemEntities] of Object.entries(systemGroups)) {
const app = this._buildApplication(sourceSystem, systemEntities, relationships);
applications.push(app);
}

return {
applications,
custom_property_definition: this._buildPropertyDefinitions()
};
}

private _buildApplication(
sourceSystem: string,
entities: Entity[],
relationships: Relationship[]
): OAAApplication {
const app: OAAApplication = {
name: this._formatAppName(sourceSystem),
application_type: sourceSystem,
local_users: [],
local_groups: [],
local_roles: [],
resources: [],
permissions: [],
identity_to_permissions: []
};

// Map autonomous_identity + human_identity → local_users
const identities = entities.filter(e =>
e.entity_type === 'autonomous_identity' || e.entity_type === 'human_identity'
);

for (const identity of identities) {
const user: OAALocalUser = {
name: identity.display_name,
unique_id: identity.entity_id,
identities: this._extractIdentities(identity),
is_active: identity.status === 'active',
created_at: identity.created_at,
last_login_at: identity.properties.last_activity_at
};

// Custom properties
if (identity.entity_type === 'autonomous_identity') {
user.custom_properties = {
identity_subtype: identity.properties.identity_subtype,
execution_mode: identity.properties.execution_mode,
security_relevance: identity.properties.security_relevance
};
}

app.local_users.push(user);
}

// Map roles
const roles = entities.filter(e => e.entity_type === 'role');
for (const role of roles) {
app.local_roles.push({
name: role.display_name,
unique_id: role.entity_id,
permissions: [] // populated via HAS_ROLE + GRANTS edges
});
}

// Map resources
const resources = entities.filter(e => e.entity_type === 'resource');
for (const resource of resources) {
app.resources.push({
name: resource.display_name,
resource_type: resource.properties.resource_type,
unique_id: resource.entity_id,
custom_properties: {
business_domain: resource.properties.business_domain,
sensitivity: resource.properties.sensitivity,
contains_pii: resource.properties.contains_pii
}
});
}

// Map HAS_ROLE + GRANTS + APPLIES_TO relationships
this._mapPermissions(app, relationships);

return app;
}

private _extractIdentities(entity: Entity): string[] {
const identities: string[] = [];

// Email
if (entity.properties.email) {
identities.push(entity.properties.email);
}

// App ID (for service principals)
if (entity.properties.app_id) {
identities.push(entity.properties.app_id);
}

// User principal name
if (entity.properties.user_principal_name) {
identities.push(entity.properties.user_principal_name);
}

return identities;
}

private _formatAppName(sourceSystem: string): string {
const names = {
'servicenow': 'ServiceNow Production',
'entra_id': 'Entra ID',
'github': 'GitHub',
'aws': 'AWS'
};
return names[sourceSystem] || sourceSystem;
}

private _mapPermissions(app: OAAApplication, relationships: Relationship[]): void {
// Build permission graph: identity -[HAS_ROLE]-> role -[GRANTS]-> permission -[APPLIES_TO]-> resource
// Flatten to OAA's identity_to_permissions structure

// Implementation: traverse relationships to build permission assignments
// ...
}
}

API endpoint:

// sv0-platform/src/api/routes/export.ts
router.get('/tenants/:id/export/oaa', async (req, res) => {
const tenantId = req.params.id;
const exporter = new OAAExporter(storageAdapter);

try {
const payload = await exporter.exportTenant(tenantId);
res.json(payload);
} catch (error) {
logger.error('OAA export failed', { tenantId, error });
res.status(500).json({ error: 'Export failed' });
}
});

Usage (submit to Veza):

# Export OAA payload
curl -H "X-Api-Key: $SV0_API_KEY" \
https://sv0-platform.example.com/api/v1/tenants/default/export/oaa \
> sv0-oaa-export.json

# Submit to Veza
python submit_to_veza.py --payload sv0-oaa-export.json
# submit_to_veza.py
from oaaclient.client import OAAClient
import json
import sys

veza_url = os.getenv("VEZA_URL")
veza_api_key = os.getenv("VEZA_API_KEY")

with open(sys.argv[2]) as f:
payload = json.load(f)

veza = OAAClient(url=veza_url, token=veza_api_key)

provider = veza.get_provider("SecurityV0")
if not provider:
provider = veza.create_provider("SecurityV0", "application")

for app in payload["applications"]:
data_source = veza.get_data_source(provider["id"], app["name"])
if not data_source:
data_source = veza.create_data_source(provider["id"], app["name"], "SecurityV0")

veza.push_application(provider["name"], data_source["name"], application_object=app)
print(f"Submitted {app['name']} to Veza")

Pros:

  • Clean separation: connectors unchanged, OAA is an export format
  • Platform controls the transformation (single source of truth)
  • OAA export can be improved over time without touching connectors
  • Optional: only run if Veza integration is needed

Cons:

  • Not real-time (OAA export is a separate step from connector sync)
  • Cross-system execution chains still not fully represented in OAA

Effort: 5-7 days (OAA exporter + API endpoint + tests)


Connector emits both NormalizedGraph and OAA payload.

# sv0-connectors/integrations/entra-servicenow/transformer.py
from oaaclient.templates import CustomApplication, OAAPermission

class EntraServiceNowTransformer:
def transform(self, discovered: DiscoveredEntities) -> dict:
# Existing: build NormalizedGraph
normalized_graph = self._build_normalized_graph(discovered)

# New: build OAA payload
oaa_payload = self._build_oaa_payload(discovered)

return {
"normalized_graph": normalized_graph,
"oaa": oaa_payload
}

def _build_oaa_payload(self, discovered: DiscoveredEntities) -> dict:
# ServiceNow Application
sn_app = CustomApplication("ServiceNow Production", application_type="servicenow")

# Define custom properties
sn_app.property_definitions.define_local_user_property("identity_subtype", OAAPropertyType.STRING)
sn_app.property_definitions.define_local_user_property("execution_mode", OAAPropertyType.STRING)

# Map ServiceNow users (including integration users)
for sn_user in discovered.sn_users:
user = sn_app.add_local_user(
sn_user['user_name'],
unique_id=sn_user['sys_id'],
identities=[sn_user.get('email')] if sn_user.get('email') else []
)
user.set_property("identity_subtype", self._infer_identity_subtype(sn_user))
user.is_active = sn_user.get('active', True)

# Map ServiceNow roles
for role_name in discovered.roles:
sn_app.add_local_role(role_name)

# Map ServiceNow resources
for table_name in discovered.tables:
resource = sn_app.add_resource(table_name, resource_type="table")
# Custom properties from discovered metadata
if table_name in discovered.table_metadata:
metadata = discovered.table_metadata[table_name]
resource.set_property("business_domain", metadata.get('domain', 'unknown'))

# Map permissions (HAS_ROLE + GRANTS + APPLIES_TO)
for user_role_assignment in discovered.user_role_assignments:
user = sn_app.local_users.get(user_role_assignment['user_id'])
if user:
user.add_role(user_role_assignment['role_name'])

# Entra ID Application (separate)
entra_app = CustomApplication("Entra ID", application_type="entra_id")

for sp in discovered.azure_sps:
user = entra_app.add_local_user(
sp['displayName'],
unique_id=sp['id'],
identities=[sp['appId']] # Use appId as identity
)
user.set_property("identity_subtype", "service_principal")

# Map Entra roles
for role in discovered.azure_roles:
entra_app.add_local_role(role['displayName'], unique_id=role['id'])

# Problem: OAuth → SP authentication chain not representable
# OAA has no cross-application auth concept
# Loss of cross-system execution paths

return {
"applications": [
sn_app.to_dict(),
entra_app.to_dict()
]
}

Platform ingestion changes:

// sv0-platform/src/ingestion/sync-ingestion.ts
async function processConnectorOutput(output: ConnectorOutput) {
// Process NormalizedGraph (existing)
if (output.normalized_graph) {
await diffEngine.computeAndApply(tenantId, syncId, output.normalized_graph);
}

// Process OAA payload (new)
if (output.oaa && config.veza_integration_enabled) {
await submitToVeza(output.oaa);
}
}

async function submitToVeza(oaa: OAAPayload) {
// Submit directly to Veza from platform
const veza = new OAAClient(config.veza_url, config.veza_api_key);

for (const app of oaa.applications) {
await veza.push_application("SecurityV0", app.name, app);
}
}

Pros:

  • Veza integration is automatic (part of connector sync)
  • No separate export step

Cons:

  • Double schema maintenance — every connector change requires updating both NormalizedGraph and OAA builders
  • Connector complexity doubles — 8-10 days per connector (initial), +30% ongoing maintenance
  • Cross-system chains lost — ServiceNow and Entra are separate Applications in OAA
  • Bloated connector output — storing both formats in sync records

Effort: 8-10 days per connector (initial), +3-4 days per significant connector change

Verdict: Not worth it. Approach B achieves the same Veza integration with zero connector changes.


8. Practical Recommendation

Keep NormalizedGraph, Build OAA Adapter

Decision:

  1. Connectors continue to emit NormalizedGraph only — no OAA-specific code in connectors
  2. Platform provides OAA export API/api/v1/tenants/:id/export/oaa
  3. Optional Veza integration — if customers want Veza, they run OAA export + submit script

Rationale:

ConcernHow This Addresses It
"OAA has application notion"OAA adapter maps each source system to an OAA Application
"Script include, business flow looks like an application"These are modeled as Resources within the source system Application (e.g., ServiceNow)
"Autonomous execution could be a collection of applications"Execution chains are stored in execution_chains collection (NormalizedGraph), optionally exported as custom OAA resources
Cross-system auth chainsPreserved in NormalizedGraph (AUTHENTICATES_TO edges), lossy in OAA export (documented limitation)
Veza compatibilityOAA export provides standard-compliant payload for Veza ingestion
Neo4j portabilityNormalizedGraph maps directly to Neo4j nodes/edges (future)

Limitations of OAA export (documented):

SecurityV0 FeatureOAA RepresentationLoss
Cross-system execution chainsSplit into separate Applications per source systemNo single-query path from BR → SP → Entra roles
AUTHENTICATES_TO edgesNot represented (IdP correlation only)Cross-system auth requires manual correlation
Credential entitiesMapped to custom properties on local_usersNo credential lifecycle tracking
Execution evidenceNot includedNo proof of actual execution vs. authority
RUNS_AS, TRIGGERS_ON edgesNot representedAutomation identity binding lost
Temporal diffOAA is snapshot-onlyNo change detection in Veza (full replace each sync)

What customers get with OAA export:

  • Per-system authorization view (ServiceNow users → roles → resources, Entra SPs → roles → permissions)
  • IdP correlation (if email addresses match, Veza links identities)
  • Resource classification (business domain, sensitivity)
  • Custom properties (identity_subtype, execution_mode, security_relevance)

What they lose:

  • Deterministic cross-system execution paths (SecurityV0's core differentiator)
  • Temporal drift detection (when did roles change?)
  • Execution evidence (is this identity actually active?)
  • Automation chain tracking (which automations exist, how have they changed?)

When to use OAA export:

  • Customer already has Veza and wants SecurityV0 data in their existing dashboard
  • Need integration with Veza's access review workflows
  • Want IdP correlation (linking service principals to human owners via Okta)

When OAA export is NOT sufficient:

  • Need to answer "what can this Entra SP do in ServiceNow?" (requires NormalizedGraph + AUTHENTICATES_TO)
  • Need temporal drift analysis ("when did this automation gain HR access?")
  • Need execution evidence ("is this dormant or actively executing?")
  • Need chain-level findings (orphaned automation chains)

Implementation plan:

PhaseDeliverableEffort
Phase 1OAA export API skeleton (empty payload)1 day
Phase 2Map entities → OAA local_users, local_roles, resources2 days
Phase 3Map relationships → OAA permissions2 days
Phase 4Custom properties (identity_subtype, business_domain, etc.)1 day
Phase 5Submit script (Python wrapper for Veza API)1 day
Phase 6Tests + documentation1 day
Total7-8 days

Future enhancements:

  • Automation webhook: trigger OAA export on sync completion (optional)
  • Chain export: represent execution chains as custom OAA Resources within a meta-Application (requires OAA schema extension)
  • Veza SDK integration: platform submits directly to Veza (no external script)

9. Answer to Core Questions

Q1: Connector Output Transformation

Can we output both NormalizedGraph AND OAA-compatible JSON from the same connector?

Yes, but not recommended. Approach A (dual-output) is technically feasible but doubles connector complexity and maintenance burden.

Better approach: Approach B (platform OAA adapter). Connectors emit only NormalizedGraph, platform exports OAA on demand.

Mapping: NormalizedNode.entity_type → OAA entity type

NormalizedNode.entity_typeOAA Entity TypeNotes
autonomous_identitylocal_userSet custom property identity_subtype = service_principal, business_rule, etc.
human_identitylocal_userIf email exists, used for IdP correlation
rolelocal_roleDirect mapping
permissioncustom_permissionMap to OAA's 10 canonical types
resourceCustomResourceDirect mapping, nest sub-resources if hierarchy exists
credentialCustom property on local_userNo first-class OAA credential entity
execution_evidenceNot mappedOAA has no execution tracking

Q2: OAA SDK Integration

Should SecurityV0 connectors:

  • ❌ Use the OAA SDK directly to build ApplicationObjects? No — couples connectors to Veza
  • ✅ Build our own NormalizedGraph and convert to OAA at the platform level? Yes — clean separation
  • ❌ Maintain a dual-output connector that speaks both protocols? No — too much complexity

Reasoning:

  • NormalizedGraph is a superset of OAA (supports features OAA doesn't have)
  • Transforming NormalizedGraph → OAA is lossy but one-way (can't reverse)
  • Platform-level transformation is centralized (one implementation vs. N connectors)
  • Connectors stay database-agnostic (work with MongoDB, Neo4j, OAA, or custom storage)

Q3: Resource Hierarchy Design

If automations are Resources within the source application:

sn_app = CustomApplication("ServiceNow Production", application_type="servicenow")

# Automations as a resource collection
automations = sn_app.add_resource("Automations", resource_type="collection")

# Individual automations as sub-resources
azure_graph_br = automations.add_sub_resource(
"AzureGraphRouter-BR",
resource_type="business_rule"
)

Problem: How do cross-system resources (Entra SP) reference back?

Answer: They don't. OAA Applications are isolated. The Entra SP lives in a separate Entra ID Application. Veza may correlate them via IdP, but there's no explicit cross-application execution path.

Python code (flattened, 2-level hierarchy):

from oaaclient.templates import CustomApplication, CustomResource

sn_app = CustomApplication("ServiceNow Production", application_type="servicenow")

# Tables as top-level resources
incident_table = sn_app.add_resource("incident", resource_type="table")

# Automations as top-level resource collection
automations = sn_app.add_resource("Automations", resource_type="automation_collection")

# Business Rules as sub-resources under Automations
br1 = automations.add_sub_resource("AzureGraphRouter-BR", resource_type="business_rule")
br2 = automations.add_sub_resource("IncidentNotifier-BR", resource_type="business_rule")

# Custom properties on sub-resources
br1.set_property("trigger_table", "incident")
br1.set_property("destination", "graph.microsoft.com")
br1.set_property("cross_system", True)

# Limitation: Can't nest deeper (Script Includes called by BR1 can't be sub-sub-resources)
# Workaround: Make Script Includes siblings under Automations
si1 = automations.add_sub_resource("AzureGraphRouter-SI", resource_type="script_include")
si1.set_property("called_by", "AzureGraphRouter-BR")

Q4: Application-per-Chain Design

If each automation chain is its own Application:

Problem: Connectors don't know chain boundaries. Platform assembles chains.

Implication: Only the platform can build Application-per-chain, not connectors.

But: OAA connectors are independent — each connector pushes its own Applications to Veza. There's no "platform aggregates Applications from multiple connectors" pattern.

Workaround: Platform submits a meta-Application called "Automation Chains":

# Platform OAA exporter builds this
chains_app = CustomApplication("SecurityV0 Automation Chains", application_type="execution_chains")

# Each chain is a resource
chain1 = chains_app.add_resource("AzureGraphRouter", resource_type="automation_chain")
chain1.set_property("entry_point", "sn-br-abc")
chain1.set_property("destination", "graph.microsoft.com")
chain1.set_property("cross_system", True)

# Chain members as sub-resources? (OAA limitation: only 2 levels)
# Or custom properties listing member entity IDs
chain1.set_property("member_entities", ["sn-br-abc", "sn-si-def", "entra-sp-123"])

Python code:

from oaaclient.templates import CustomApplication

def build_automation_chains_app(chains: list[ExecutionChain]) -> CustomApplication:
app = CustomApplication("SecurityV0 Automation Chains", application_type="execution_chains")

for chain in chains:
chain_resource = app.add_resource(
chain.display_name,
resource_type="automation_chain",
unique_id=chain.chain_id
)

# Metadata
chain_resource.set_property("entry_point_id", chain.anchor_entity_id)
chain_resource.set_property("entry_point_name", chain.entry_point_name)
chain_resource.set_property("trigger_pattern", chain.trigger_pattern)
chain_resource.set_property("destination", chain.egress_pattern.destinations)
chain_resource.set_property("cross_system", chain.is_cross_system)
chain_resource.set_property("ownership_status", chain.ownership_status)
chain_resource.set_property("blast_radius_domains", chain.blast_radius_domains)

# Member entities (as property array, since OAA sub-resources don't support 3+ levels)
chain_resource.set_property("member_entity_ids", [
ref.entity_id for ref in chain.entity_refs
])

return app

Usage:

// Platform export
const chains = await db.collection('execution_chains').find({ tenant_id: tenantId }).toArray();
const chains_app = buildAutomationChainsApp(chains);

// Include in OAA export alongside system Applications
return {
applications: [
serviceNowApp,
entraIdApp,
chains_app // Meta-application
]
};

Veza display:

  • Application: "SecurityV0 Automation Chains"
  • Resources: Each chain as a resource with custom properties
  • Limitation: Can't drill down to individual chain members (would need to correlate back to ServiceNow/Entra Applications by entity_id)

Q5: Cross-System Challenge

How does OAA handle cross-application resource references?

Answer: It doesn't. OAA Applications are isolated authorization domains. Cross-app relationships are inferred via IdP correlation (email matching), not explicit graph edges.

How do OAA community connectors handle this?

Examined: GitHub, GitLab, Looker, PagerDuty connectors

Findings:

  • GitHub: Single-system (GitHub users → GitHub repos), no cross-system scenarios
  • GitLab: Single-system (GitLab users → GitLab projects), no cross-system scenarios
  • Looker: Models connect to databases (Snowflake, Redshift), but database is a separate OAA Application. Looker connector models connections as sub-resources, does NOT reference the Snowflake Application.
  • PagerDuty: Integrations with AWS, ServiceNow modeled as resources, but target systems are separate Applications. No cross-app execution path.

Pattern: OAA connectors treat each system as isolated. If System A calls System B, that's two separate Applications with no link.

Implication for SecurityV0: The Entra SP → ServiceNow OAuth → ServiceNow user chain would be split:

  • Entra Application: SP with roles X, Y, Z
  • ServiceNow Application: Integration user with roles A, B, C
  • NO CONNECTION between SP and integration user (unless email-based IdP correlation works, which it won't for service principals)

SecurityV0's differentiator (deterministic cross-system execution paths) is not expressible in standard OAA.


Q6: Compatibility Matrix

See Section 6 above.

Summary:

  • Approach A (Dual-Output): Supports Veza but high connector complexity
  • Approach B (Platform OAA Adapter): Best balance — zero connector changes, clean OAA export, documented limitations
  • Approach C (OAA-Native): Not viable — loses all SecurityV0 differentiators

Recommended: Approach B with clear documentation of what OAA export can and cannot represent.

Connector changes required: None

Platform changes required: OAA export API (~7 days)


10. Files Produced

  • /Users/lucky/dev/securityv0/sv0-documentation/docs/analysis/2026-02-12-automation-classification/04-oaa-mapping-integrator.md (this document)

11. Next Steps

  1. Founder decision: Approach B (platform OAA adapter) vs. Approach A (dual-output) vs. abandon OAA compatibility?
  2. If Approach B approved:
    • Create ticket: Implement OAA export API
    • Deliverable: /api/v1/tenants/:id/export/oaa endpoint
    • Tests: Validate OAA schema compliance
    • Documentation: What OAA export can/cannot represent
    • Python submit script: Wrapper for Veza API integration
  3. If cross-system execution paths must be in Veza: Explore OAA schema extensions (custom relationship types) — would require Veza support
  4. Update connector interface spec: Clarify that OAA is an export format, not a connector output format

Round 4 analysis complete. 650+ lines examining OAA integration patterns, cross-system auth challenges, and connector architecture trade-offs. Recommendation: Platform OAA adapter with zero connector changes.