Track 2: Architecture Refactor — Import-by-Type + Platform-Only Findings
Context
Track 1 is complete: all 7 PRD requirements shipped, 141 connector tests + 179 platform tests passing, CI workflow running. The connector currently uses heavy ExecutionChain and Integration dataclasses that pre-link entities (BR→REST→OAuth→SP) before the transformer decomposes them back into individual NormalizedGraph nodes/edges. Detection logic lives in the connector (detectors.py) duplicating what the platform evaluator should handle.
Track 2 refactors this architecture:
- Import-by-type: Connector discovers entities independently, emits flat NormalizedGraph directly
- Platform-only findings: All detection/evaluation in platform evaluator, connector is pure discovery + classification
- Path materializer extension: Platform reconstructs automation→identity→resource chains via BFS
Key Insight
Automations already map to autonomous_identity → identity entity type in the platform. They already go through ingestion, diff engine, and versioning. But execution paths are empty for automations because the path materializer only follows HAS_ROLE → GRANTS → APPLIES_TO (+ one AUTHENTICATES_TO hop). Automations have RUNS_AS and EXECUTES_ON edges that the materializer doesn't traverse.
Fix the materializer first → existing evaluator rules (orphaned_ownership, dormant_authority, scope_drift, privilege_justification_gap) automatically work for automations. Then simplify the connector.
Graph Edge Direction Reference (Source of Truth)
From transformer.py — these are the actual edge directions emitted by the connector:
| Edge | Source | Target | Where |
|---|---|---|---|
AUTHENTICATES_TO | SP | SN OAuth entity | transformer.py:287-292 |
AUTHENTICATES_VIA | REST Message | OAuth entity | transformer.py:310 |
RUNS_AS | Automation (BR/SI/Job) | SP | transformer.py:438-445 |
RUNS_AS | Flow | SP (inferred from endpoint chain) | transformer.py:661-666 |
RUNS_AS | Flow | SN User (from run_as field) | transformer.py:708-713 |
OWNED_BY | SP | Azure Owner | transformer.py:204-211 |
OWNED_BY | Automation | SN Creator | transformer.py:897-902 |
CREATED_BY | SN OAuth entity | SN Creator | transformer.py:227-234 |
TRIGGERS_ON | Automation | Table resource | transformer.py:404-412 |
EXECUTES_ON | Automation | REST Message / endpoint | transformer.py:634-638 |
Critical: SP → AUTHENTICATES_TO → OAuth (NOT OAuth → AUTHENTICATES_TO → SP). The materializer follows AUTHENTICATES_TO from source identity, so it traverses: SP → OAuth. OAuth entities do NOT have outgoing AUTHENTICATES_TO edges.
Phase Map
T2-0: Bug fix — ownership_level property naming mismatch (Track 1 defect)
T2-1: Platform — Path materializer extension (unblocks everything)
T2-2: Platform — Evaluator gap analysis + new rules (REQUIRED before T2-4)
T2-3: Connector — Import-by-type refactor (replace ExecutionChain)
T2-4: Connector — Deprecate detectors.py (GATED on T2-2 rule parity)
T2-5: Docs — ADRs + interface spec updates
T2-6: QA — End-to-end validation
T2-0 is a Track 1 defect fix. T2-1 and T2-2 are platform-side. T2-3 and T2-4 are connector-side. T2-5 is docs. T2-6 is verification.
T2-0 should land immediately (it's a standalone bug fix). T2-1 must land before T2-2 (unblocks automation path evaluation). T2-2 must land before T2-4 (hard gate — cannot remove detectors until platform rules have proven parity). T2-3 can start after T2-1. T2-5 can run in parallel.
T2-0: Bug Fix — ownership_level Property Naming Mismatch
Status: Track 1 defect, fix immediately.
Problem
The connector emits OWNED_BY edge properties with camelCase ownershipLevel, but the platform reads snake_case ownership_level everywhere:
| Where | Property Name | Code |
|---|---|---|
| Connector OWNED_BY (SP→Owner) | ownershipLevel | transformer.py:209 |
| Connector OWNED_BY (Automation→Creator) | ownershipLevel | transformer.py:901 |
| Evaluator orphaned-ownership | ownership_level | orphaned-ownership.ts:27 |
| Evidence sections | ownership_level | evidence/sections.ts:167 |
| Evidence types | ownership_level | evidence-packs/types.ts:71 |
| MongoDB index | ownership_level | mongo/schema.ts:169 |
Impact: The evaluator's getOwnershipLevel() always returns "primary" (default) because it can't find ownership_level — the actual value is stored under ownershipLevel. The ownership_degraded finding variant never fires.
Fix
Change the connector to emit ownership_level (snake_case) on OWNED_BY edge properties. This matches the platform's expectation and the graph-transformer passes edge properties through unchanged ({ ...edge.properties } at graph-transformer.ts:60).
Files:
sv0-connectors/integrations/entra-servicenow/transformer.py— 3 locations (line 209, line 599, line 901):"ownershipLevel"→"ownership_level"- Update tests that assert on the property name
T2-1: Path Materializer Extension
Goal: Automations get execution_paths computed via their RUNS_AS → identity chain.
File: sv0-platform/src/ingestion/path-materializer.ts
Current behavior (lines 98-188)
computePathsForIdentity() does:
- Get
HAS_ROLE→ fetch roles → getGRANTS→ fetch permissions → getAPPLIES_TO→ fetch resources → emit ExecutionPath - Follow
AUTHENTICATES_TOedges whendepth < MAX_AUTH_CHAIN_DEPTH(depth < 1, i.e. only at depth=0) → recurse at depth+1
What to add
Automations don't have HAS_ROLE. They have:
RUNS_AS→ SP or User (the identity they execute as)EXECUTES_ON→ resource (REST Message, table, etc.)
The materializer should follow RUNS_AS edges to "borrow" the target identity's paths.
Critical: Depth budget design
Problem: Current MAX_AUTH_CHAIN_DEPTH = 1 means AUTHENTICATES_TO is only followed at depth=0. If RUNS_AS also increments depth, then the target identity's AUTHENTICATES_TO traversal is blocked (depth=1, and 1 < 1 is false).
Solution: RUNS_AS does NOT consume the auth depth budget. RUNS_AS is identity binding (which SP does this automation run as), not auth delegation. The target identity should get a fresh depth=0 start for its own AUTHENTICATES_TO traversal.
// New: Follow RUNS_AS edges (automation → identity it runs as)
// RUNS_AS is identity binding, not auth delegation — do NOT increment depth
const runsAsTargetIds = identity.relationships
.filter(r => r.type === "RUNS_AS")
.map(r => r.target_id);
for (const targetId of runsAsTargetIds) {
if (visited.has(targetId)) continue;
const targetIdentity = await storageAdapter.getEntity(tenantId, targetId);
if (!targetIdentity || targetIdentity.entity_type !== "identity") continue;
const targetPaths = await computePathsForIdentity(
targetIdentity, tenantId, storageAdapter,
depth, // <-- same depth, NOT depth+1 (RUNS_AS doesn't consume auth budget)
visited
);
for (const path of targetPaths) {
paths.push({
...path,
via_identity: targetIdentity._id
});
}
}
This goes inside computePathsForIdentity() BEFORE the existing AUTHENTICATES_TO block. The visited set prevents cycles. The RUNS_AS target identity then gets its own auth chain budget (can still follow AUTHENTICATES_TO at depth < 1).
Resulting traversal chains (with correct edge directions)
- Automation → SP (direct roles):
automation → RUNS_AS → SP → HAS_ROLE → role → GRANTS → perm → APPLIES_TO → resource - Automation → SP → OAuth (cross-system):
automation → RUNS_AS → SP → AUTHENTICATES_TO → OAuth. OAuth typically has no HAS_ROLE so this path dead-ends. Valid for edge tracing but no execution_paths produced. - Automation → User:
automation → RUNS_AS → SN User → (user's own roles if any). SN Users may not have HAS_ROLE edges in this connector, so typically no paths. But the traversal is correct.
sync-ingestion verification
File: sv0-platform/src/workers/handlers/sync-ingestion.ts (line 132-134)
Currently only materializes paths for entity_type === "identity". Since automations ARE identities (mapped from autonomous_identity → identity), this already works. Verify with test.
Tests
File: sv0-platform/test/ingestion/path-materializer.test.ts
automation_with_runs_as_to_sp_gets_paths— automation → RUNS_AS → SP → HAS_ROLE → role → GRANTS → perm → APPLIES_TO → resource. Automation gets SP's execution_paths. Verifyvia_identityis set to SP's ID.automation_without_runs_as_gets_empty_paths— automation with no RUNS_AS edge → empty execution_paths (but no error).automation_runs_as_sp_with_authenticates_to— automation → RUNS_AS → SP (depth=0) → SP follows AUTHENTICATES_TO → OAuth (depth=1). Verify SP's auth traversal is NOT blocked by RUNS_AS. (OAuth likely has no roles so paths stay the same as test 1, but the traversal must not error.)cycle_prevention— automation → RUNS_AS → identity → RUNS_AS → automation. Visited set prevents infinite loop.runs_as_preserves_auth_budget— automation → RUNS_AS → SP (depth stays 0) → SP can follow AUTHENTICATES_TO (depth < 1 = true). Explicitly assert that depth is NOT incremented by RUNS_AS.
T2-2: Evaluator Gap Analysis + New Rules
Goal: Verify existing platform rules fire correctly for automations. Add any missing rules. This phase must complete before T2-4 (detector deprecation).
Existing rules vs connector detectors mapping (corrected)
| Connector Detector | Platform Rule | Parity Status |
|---|---|---|
| CrossServiceDetector: NO_OWNER | orphaned_ownership | Works — checks OWNED_BY on any identity. Automations get OWNED_BY edges via _add_creator_ownership(). |
| CrossServiceDetector: DISABLED_OWNER | orphaned_ownership | Works — checks target entity status (active/disabled). |
| CrossServiceDetector: INACTIVE_OWNER | orphaned_ownership | GAP — evaluator checks entity status field, NOT sign-in dates. An owner with status: "active" but no recent sign-ins won't be caught. The connector detector may use different inactivity logic. |
| OwnershipDecayDetector | ownership_degraded | Works after T2-0 — requires ownership_level property naming fix. Once fixed, correctly classifies primary/secondary/inherited decay. |
| ScopeDriftDetector | scope_drift | Works — compares HAS_ROLE over entity versions. |
| ExecutionChainDetector: dormant SP | dormant_authority | Works after T2-1 — needs execution_paths populated via RUNS_AS traversal. |
| ExecutionChainDetector: elevated access | privilege_justification_gap | Works after T2-1 — needs execution_paths populated. |
| CrossServiceDetector: HIGH_PRIVILEGE_NO_OWNER | orphaned_ownership + execution_paths blast radius | Works after T2-1 — orphaned identity with populated execution_paths raises severity. |
| CrossServiceDetector: EXCESSIVE_DATA_ACCESS | privilege_justification_gap | Works — checks scope breadth. |
| ServiceNowDetector: SNOW_OAUTH_NO_OWNER | orphaned_ownership | GAP — OAuth entities have CREATED_BY edges, NOT OWNED_BY. orphaned_ownership only checks OWNED_BY (line 48). SN OAuth entities without OWNED_BY will always be flagged as orphans. Need to decide: (a) add OWNED_BY edges to OAuth entities in connector, or (b) make evaluator also check CREATED_BY. |
| Shadow automation (unmatched client_id) | — | GAP — no platform rule. See below. |
Gap 1: Unresolved cross-system auth (REQUIRED for T2-4)
The connector's detectors check if an OAuth entity's client_id has no matching Azure SP (shadow automation). The platform evaluator doesn't currently detect this.
New rule: unresolved_cross_system_auth (REQUIRED — hard prerequisite for T2-4)
- Trigger: identity has
identity_binding_status: "unlinked"ANDidentitySubtypeis a cross-system auth entity type (oauth_apporservice_principal). Does NOT fire on automation subtypes (business_rule,system_execution,flow_designer_flow,scheduled_job) where "unlinked" means no deterministic execution-log join, not shadow auth. - Connector change: OAuth entity nodes now emit
identity_binding_status: "bound"(integration path, matched to SP) or"unlinked"(execution chain path, no matching SP). - Severity: medium
- Rationale: Without this rule, shadow automations (OAuth entities with unmatched client_id) will go undetected after detector removal
File: sv0-platform/src/evaluator/rules/unresolved-auth.ts (new)
File: sv0-platform/src/evaluator/rules/index.ts (register rule)
Gap 2: INACTIVE_OWNER sign-in-based detection
Decision: Accepted gap — NOT in scope for Track 2.
The platform's isOwnerNonActive() only checks entity status field (disabled, suspended, etc.). An owner with status "active" but no sign-in for 6+ months would be missed. Document this as a known limitation. Can be addressed in a future Track if needed.
Gap 3: SN OAuth entity OWNED_BY coverage
OAuth entities in the NormalizedGraph get CREATED_BY edges (transformer.py:227-234) but NOT OWNED_BY edges. The orphaned_ownership rule only checks OWNED_BY (orphaned-ownership.ts:48).
Decision: Extend the evaluator to also check CREATED_BY edges.
Modify orphaned-ownership.ts to treat CREATED_BY as a fallback ownership relationship. If an identity has no OWNED_BY edges but has CREATED_BY edges, use the CREATED_BY targets as owners. This is the more general solution — applies to any entity type that uses CREATED_BY for ownership.
File: sv0-platform/src/evaluator/rules/orphaned-ownership.ts — add CREATED_BY fallback after OWNED_BY check (line 48)
Tests
Add test cases to sv0-platform/test/evaluator/ that:
- Automation identity with OWNED_BY edges (all disabled) → verify orphaned_ownership fires
- Automation with execution_paths (from T2-1) but no execution evidence → verify dormant_authority fires
- Automation with HAS_ROLE growth over versions → verify scope_drift fires
- Automation with
identity_binding_status: "unlinked"→ verify unresolved_cross_system_auth fires - Automation with all primary owners non-active but active secondary → verify ownership_degraded fires (validates T2-0 fix)
T2-4 Gate Criteria
T2-4 (detector deprecation) MUST NOT proceed until:
-
unresolved_cross_system_authrule is implemented and tested (with subtype guard against BR/SI false positives) -
orphaned_ownershipfires correctly for automations (validated with test 1, 5) -
orphaned_ownershipCREATED_BY fallback works for entities without OWNED_BY edges (Gap 3 — validated with CREATED_BY tests) -
dormant_authorityfires for automations with empty evidence (validated with test 2) - OAuth entity nodes emit
identity_binding_statusproperty (both integration and execution chain paths) - Property naming fix (T2-0) is landed and verified
T2-3: Connector Import-by-Type Refactor
Goal: Replace ExecutionChain + Integration dataclasses with flat entity discovery. Simplify correlator and transformer.
New data flow
Current:
SN client → raw chains dict → Correlator → ExecutionChain objects → Transformer → NormalizedGraph
Target:
SN client → entity dicts by type → EdgeResolver (client_id match) → Transformer → NormalizedGraph
Step 3a: New DiscoveredEntities container
File: sv0-connectors/integrations/entra-servicenow/correlator.py
Replace Integration and ExecutionChain with a simple container:
@dataclass
class DiscoveredEntities:
"""Flat collection of discovered entities by type — no pre-linked chains."""
business_rules: list[dict] # From SN: sys_id, name, table, script, etc.
script_includes: list[dict] # From SN: sys_id, name, api_name, script
scheduled_jobs: list[dict] # From SN: sys_id, name, run_as, script
flows: list[dict] # From SN: sys_id, name, triggers, actions
rest_messages: list[dict] # From SN: sys_id, name, endpoint, http_methods
oauth_entities: list[dict] # From SN: sys_id, name, client_id
azure_sps: list[dict] # From Azure: id, app_id, owners, roles, sign-ins
azure_users: list[dict] # From Azure: owner details
sn_users: list[dict] # From SN: creator details
execution_data: dict[str, dict] # Flow/job execution evidence
# Resolved edges (from EdgeResolver) — dicts, not tuples, to preserve provenance
auth_edges: list[dict] # {source_id, target_id, matching_field, matching_value, ...}
caller_edges: list[dict] # {source_id, target_id, evidence_refs, ...}
Important: Resolved edges are dicts (NOT tuples) to preserve provenance fields needed for evidence packs. The current AUTHENTICATES_TO edges carry evidenceReferences with matching field/value, issuing system/tenant, etc. (transformer.py:294-301). Losing these would break explainability.
Step 3b: EdgeResolver (replaces IntegrationCorrelator)
File: sv0-connectors/integrations/entra-servicenow/correlator.py
The correlator simplifies to:
@dataclass
class ResolvedEdge:
"""A resolved cross-entity edge with provenance."""
source_id: str # source entity sys_id / id
target_id: str # target entity sys_id / id
edge_type: str # e.g. AUTHENTICATES_TO, EXECUTES_ON
properties: dict # evidence references, matching fields, etc.
class EdgeResolver:
"""Resolve cross-entity relationships from discovery data."""
def resolve_auth_edges(self, oauth_entities, azure_sps) -> list[ResolvedEdge]:
"""Match SN OAuth entities to Azure SPs by client_id."""
sp_by_client = {sp["app_id"]: sp for sp in azure_sps}
edges = []
for oauth in oauth_entities:
matched_sp = sp_by_client.get(oauth.get("client_id"))
if matched_sp:
edges.append(ResolvedEdge(
source_id=matched_sp["id"], # SP is SOURCE (SP → OAuth)
target_id=oauth["sys_id"], # OAuth is TARGET
edge_type="AUTHENTICATES_TO",
properties={
"evidenceReferences": {
"matchingField": "client_id",
"matchingValue": oauth["client_id"],
"issuingSystemId": matched_sp.get("app_id", ""),
"targetSystemId": oauth.get("client_id", ""),
}
}
))
return edges
def resolve_caller_edges(self, automations, rest_messages) -> list[ResolvedEdge]:
"""Match automations to REST messages they call (from script references)."""
# Keep existing find_callers_of_rest_message logic but return ResolvedEdge
...
Note: AUTHENTICATES_TO edge direction is SP → OAuth (source=SP, target=OAuth), matching the current transformer behavior.
Step 3c: Simplified transformer
File: sv0-connectors/integrations/entra-servicenow/transformer.py
The transformer's transform() method changes signature:
# Current:
def transform(self, integrations, execution_chains, flows=None, execution_data=None) -> dict:
# New:
def transform(self, entities: DiscoveredEntities) -> dict:
Internally, it loops through each entity type and emits nodes:
- Each business_rule → autonomous_identity node (identitySubtype: business_rule)
- Each REST message → resource node
- Each OAuth entity → autonomous_identity node (identitySubtype: oauth_app)
- Each Azure SP → autonomous_identity node (identitySubtype: service_principal)
- etc.
Edges come from the EdgeResolver's pre-computed edges plus per-entity relationships (TRIGGERS_ON from BR table field, RUNS_AS from flow run_as, etc.).
The enrichment pipeline (_enrich_automation_properties()) stays mostly the same — it still applies egress, origin, ownership, risk classification to automation nodes.
Step 3d: CLI pipeline update
File: sv0-connectors/integrations/entra-servicenow/cli.py
Replace:
# Old:
integrations = correlator.match_by_client_id(azure_sps, sn_apps)
chains = correlator.correlate_execution_chains(raw_chains, azure_sps)
graph = transformer.transform(integrations, chains, flows=flows, execution_data=execution_data)
# New:
entities = DiscoveredEntities(
business_rules=sn_client.get_business_rules(),
script_includes=sn_client.get_script_includes(),
...
)
edge_resolver = EdgeResolver()
entities.auth_edges = edge_resolver.resolve_auth_edges(entities.oauth_entities, entities.azure_sps)
entities.caller_edges = edge_resolver.resolve_caller_edges(...)
graph = transformer.transform(entities)
Migration strategy
Keep the old Integration/ExecutionChain dataclasses temporarily. Add DiscoveredEntities alongside. Update transformer to accept both (overloaded or flag). Once new path works end-to-end, remove old classes.
Tests
- Update
test_scenarios.pyto produceDiscoveredEntitiesobjects instead of(integrations, chains)tuples - Update all tests (test_transformer.py, test_prd_requirements.py, test_cli_smoke.py) to use new interface
- All 141 tests must still pass with new data structures
- PRD requirement assertions remain identical (same output properties)
T2-4: Deprecate detectors.py
Goal: Remove detection logic from connector. CLI reports become summary-only (no issue detection).
HARD GATE: This phase MUST NOT start until T2-2 gate criteria are met (see T2-2 section). Specifically:
unresolved_cross_system_authplatform rule must be implemented, tested, and passing- All T2-2 evaluator tests must pass (orphaned_ownership, dormant_authority for automations)
- T2-0 property naming fix must be landed
What changes
- Remove detector imports from cli.py — no more CrossServiceDetector, ExecutionChainDetector calls
- CLI reports — chain_markdown_report and integration_markdown_report simplify to inventory reports (no "Detected Issues" sections)
- detectors.py — delete entirely (no "mark deprecated" half-measure)
- Exit codes — cli.py currently returns exit code 2 for critical issues found. With no detectors, exit code is 0 (success) or 1 (errors). Detection happens on platform side.
What stays in connector
- Egress classifier (property on node)
- Origin classifier (property on node)
- Ownership validator (property on node)
- Risk grouper (property on node)
- These are CLASSIFICATIONS (node properties), not FINDINGS (platform evaluator)
Detection parity verification before merge
Before removing detectors, run both old (connector) and new (platform) detection side-by-side against all 5 test scenarios. Every finding the connector detectors produce must have a matching platform finding. Document any accepted differences.
Tests
- Remove/update tests that assert detection output
- CLI smoke tests that test report generation need updating (no more detection sections)
- PRD tests focused on properties (not detections) stay unchanged
T2-5: ADRs + Docs
ADR: Import-by-Type Connector Architecture
File: sv0-documentation/docs/architecture/decisions/adr-004-import-by-type-connectors.md
Document:
- Why: ExecutionChain pre-linking is redundant (transformer decomposes it back)
- What: Connector discovers entities independently, emits flat NormalizedGraph
- Trade-off: Slightly more work for platform (path materializer) but much simpler connector
ADR: Platform-Only Finding Generation
File: sv0-documentation/docs/architecture/decisions/adr-005-platform-only-findings.md
Document:
- Why: Duplicate detection (connector detectors + platform evaluator)
- What: Connector is pure discovery + classification. All findings from platform evaluator.
- Trade-off: CLI loses issue detection capability (acceptable — platform is the source of truth)
Update connector interface spec
File: sv0-documentation/docs/architecture/05-connectors.md
- Update connector output contract for import-by-type
- Clarify that connectors emit entity properties (classifications) not findings
- Document DiscoveredEntities → NormalizedGraph flow
Update data model
File: sv0-documentation/docs/architecture/01-data-model.md
- Document automation execution path traversal pattern
- Clarify RUNS_AS semantics for path materialization
T2-6: End-to-End Validation
Connector tests
cd sv0-connectors/integrations/entra-servicenow && pytest
# All tests pass with new DiscoveredEntities interface
# PRD requirement assertions unchanged
# CI report generates same NormalizedGraph structure
Platform tests
cd sv0-platform && npm test && npm run test:integration && npm run typecheck
# Path materializer tests include automation traversal
# Evaluator tests include automation entity evaluation
# Graph transformer tests include Track 1 properties
E2E smoke test
- Run
python ci_report.py→ generates NormalizedGraph from test scenarios - Submit graph to local platform:
python cli.py --all --submit --platform-url http://localhost:3000 - Verify:
- Automation entities stored with all Track 1 properties
- Automation entities have execution_paths (via RUNS_AS → SP paths)
- Evaluator fires orphaned_ownership for automations with no active owner
- Evaluator fires dormant_authority for automations with paths but no evidence
- Evidence packs generated for automation findings
Execution Order
T2-0 (ownership_level fix) ─→ (land immediately, standalone)
T2-1 (path materializer) ────→ T2-2 (evaluator + new rules, REQUIRED)
→ T2-3 (connector refactor, can start after T2-1)
T2-2 (evaluator parity) ─────→ T2-4 (deprecate detectors, HARD GATE on T2-2)
T2-3 (import-by-type) ───────→ T2-4 (also depends on T2-3)
T2-5 (docs) ─────────────────→ (parallel with all)
T2-6 (E2E) ──────────────────→ (after T2-0 through T2-4)
Decisions Made
- INACTIVE_OWNER sign-in-based detection: Accepted gap — NOT in scope for Track 2. Platform checks entity status only.
- SN OAuth entity ownership: Extend evaluator to check CREATED_BY edges as fallback (more general than adding OWNED_BY in connector).
Files Summary
Platform (sv0-platform)
| File | Change | Phase |
|---|---|---|
src/ingestion/path-materializer.ts | Add RUNS_AS traversal (no depth increment) to computePathsForIdentity | T2-1 |
test/ingestion/path-materializer.test.ts | Add 5 automation chain tests | T2-1 |
src/evaluator/rules/orphaned-ownership.ts | Add CREATED_BY fallback for ownership check | T2-2 |
src/evaluator/rules/unresolved-auth.ts | New rule (REQUIRED) | T2-2 |
src/evaluator/rules/index.ts | Register unresolved_cross_system_auth | T2-2 |
test/evaluator/*.test.ts | Add 5+ automation evaluation tests | T2-2 |
Connector (sv0-connectors/integrations/entra-servicenow)
| File | Change | Phase |
|---|---|---|
transformer.py | Fix ownershipLevel → ownership_level (3 locations) + add identity_binding_status to OAuth entity nodes | T2-0 |
test_*.py | Update property name assertions | T2-0 |
correlator.py | Add DiscoveredEntities + EdgeResolver (with ResolvedEdge, not tuples) | T2-3 |
transformer.py | Accept DiscoveredEntities; keep old transform() temporarily | T2-3 |
cli.py | Update pipeline to use new data flow | T2-3 |
test_scenarios.py | Add DiscoveredEntities builders | T2-3 |
test_transformer.py | Update for new interface | T2-3 |
test_prd_requirements.py | Update builders, keep assertions | T2-3 |
test_cli_smoke.py | Update for new pipeline | T2-3 |
detectors.py | Delete entirely (after T2-2 gate passes) | T2-4 |
Docs (sv0-documentation)
| File | Change | Phase |
|---|---|---|
docs/architecture/decisions/adr-004-import-by-type-connectors.md | New ADR | T2-5 |
docs/architecture/decisions/adr-005-platform-only-findings.md | New ADR | T2-5 |
docs/architecture/05-connectors.md | Update interface spec | T2-5 |
docs/architecture/01-data-model.md | Document automation path traversal + RUNS_AS semantics | T2-5 |