Automation Filtering + Graph Usability Strategy
Date: 2026-02-12 Status: Partially Implemented (filtering shipped 2026-02-12; graph usability solutions deferred) Trigger: Live scan of 126 entities revealed 83% false positive rate in automation inventory and unusable graph layout.
Implementation note (2026-02-12): Connector-side
internal_inventorypre-filter shipped. Addsexecution_modeandsecurity_relevanceproperties to all automation nodes. Filters out flows matching: no external egress + no identity binding + no execution evidence. See2026-02-11-reconciled-roadmap.md§2A+ for delivery details. Graph usability solutions (S1-S6) are deferred to post-pilot.Peer review corrections applied: (1) Human-triggered flows CAN be security-relevant — filter uses signal combination, not trigger type alone. (2) Graph collapse root cause is OWNED_BY + RUNS_AS fan-in, not role/permission fan-in. (3) "No execution → don't care" relaxed — dormant authority with external egress is still in scope.
Problem 1: False Positive Automations (83% noise)
Observed Data (live scan 2026-02-12)
| Metric | Count | % |
|---|---|---|
| Total entities | 126 | |
| Identity-type entities | 92 | 100% |
flow_designer_flow subtype | 83 | 90% of identities |
| RG4 + 0 exec + unlinked + no egress | 77 | 83% false positive |
| Security-relevant (has egress OR exec OR bound) | 13 | 14% |
| Truly high-priority (RG1-RG3 + active) | 6 | 7% |
Root Cause
The connector discovers all Flow Designer flows in the ServiceNow instance, including:
- System-default ITSM workflows (Change, Incident, Request, etc.)
- Internal-only flows with no external API calls
- Human-triggered flows ("User who initiates the session" run-as)
- Zero-execution flows (configured but never ran)
These are standard ServiceNow business process workflows, not autonomous security-relevant automations.
What "Automation" Means in SecurityV0
From the glossary, PRD, and architecture docs, the core security concern is:
An autonomous identity that continues executing after human ownership decays — the "Zombie NHI" problem.
An automation IS security-relevant when:
- Autonomous execution — runs without human session (scheduled, event-driven, record-change)
- Standing authority — has permissions that persist independently of any human
- External reach — can call external APIs, move data across system boundaries
- Active — has execution evidence (recent runs prove it's not just configured but running)
- Deterministic evidence — all of the above provable from first-party metadata/logs
An automation is NOT security-relevant (false positive) when:
- Human-initiated (requires click, catalog request, UI action)
- Internal-only (no egress, no cross-system data movement)
- Zero executions (configured but dormant)
- No identity binding (no SP/OAuth, no standing credential)
- System-default (out-of-box ServiceNow workflow, not custom)
Example: "Procurement Process Flow - DEFAULT"
| Signal | Value | Verdict |
|---|---|---|
| Executions (30d) | 0 | Not active |
| Egress category | none | No external reach |
| Binding status | unlinked | No standing NHI credential |
| Run-as | "User who initiates the session" | Human context, not autonomous |
| Risk group | RG4 | Lowest priority |
| Resources accessible | 0 | No blast radius |
Textbook false positive. This is a human-triggered procurement workflow — not an autonomous identity with standing execution authority.
Proposed Solution: Tiered Automation Relevance
Instead of binary include/exclude, classify automations into tiers:
Tier 1: ACTIVE THREAT SURFACE (default view)
Include if any of:
egress_categoryin["external", "llm"](RG1-RG3)identity_binding_status == "bound"(has RUNS_AS edge to SP/OAuth)execution_count_30d > 0AND has external endpoint
Expected count: ~6-10 entities from current scan
Tier 2: DORMANT AUTHORITY (show on toggle)
Include if:
- Has permissions/roles but
execution_count_30d == 0 - OR
egress_categoryin["external", "llm"]but no recent execution - Excludes RG4
Expected count: ~5-10 entities
Tier 3: INTERNAL INVENTORY (hidden by default, available via filter)
Include if:
- RG4 (internal only, no egress)
- Human-triggered flows
- Zero-execution, zero-egress, unlinked flows
Expected count: ~77 entities (the current false positives)
Implementation Options
Option A: Connector-side filtering (pre-ingest)
Add relevance scoring in transformer.py. Skip emitting nodes for Tier 3 flows.
- Pro: Reduces entity count before platform even sees them
- Con: Loses inventory completeness; can't retroactively include if criteria change
- Recommendation: Don't do this. Discovery should be broad.
Option B: Platform-side default filters (API + UI)
Keep all entities in the database. Change the default API/UI view to filter by relevance tier.
-
Add
relevance_tierproperty to automation nodes (computed during transform) -
Default Automations page filter:
relevance_tier IN [1, 2] -
"Show all" toggle reveals Tier 3
-
Graph default: exclude Tier 3
-
Pro: Full inventory preserved; user can always see everything
-
Con: Still 126 entities in DB; requires UI filter changes
-
Recommendation: This is the right approach.
Option C: Hybrid — connector tags, platform filters
Connector emits a security_relevance property on each node:
props["security_relevance"] = "active_threat" | "dormant_authority" | "internal_only"
Platform UI defaults to filtering by security_relevance != "internal_only".
- Pro: Single source of truth for relevance; no duplicate logic
- Con: Connector makes relevance judgment that could change
- Recommendation: Best balance. Use this.
Problem 2: Unusable Graph at 126+ Nodes
Observed Behavior
The graph renders as a ~8000px tall vertical line — completely unreadable. This happens because:
- 83 identity nodes (flows) all in the same Dagre rank (column 0)
- Each node takes 60px + 80px spacing = 140px vertical
- 83 nodes x 140px = 11,620px vertical extent
- All edges converge to 3-5 shared role/permission nodes
- Dagre's Sugiyama algorithm stacks same-rank nodes vertically
Current Layout Configuration
layout.ts:
rankdir: "LR" (left-to-right)
nodesep: 80 (vertical spacing between nodes in same rank)
ranksep: 200 (horizontal spacing between ranks)
NODE_WIDTH: 140
NODE_HEIGHT: 60
Browse mode loads up to 200 entities with no prefiltering. No adaptive spacing.
Current Filter Capabilities
| Filter | Browse Mode | Focus Mode |
|---|---|---|
| Entity type toggle | Yes | Yes |
| Has findings only | Yes | Yes |
| Relationship type filter | No | Yes |
| Source system filter | No | Yes |
| Depth control | No | Yes (1-3) |
| Relevance/tier filter | No | No |
Proposed Solutions (ordered by impact/effort)
S1: Default Relevance Filter in Graph (Biggest Impact, Low Effort)
With Problem 1 solved (relevance tiers):
- Graph browse mode defaults to
relevance_tier IN [1, 2] - Reduces 92 identities to ~13 security-relevant ones
- Total graph: ~40-50 entities instead of 126
- Perfectly readable with current Dagre layout
Implementation: Filter in GraphExplorerPage.tsx before passing to layout.
Effort: 2-3 hours (depends on Option C from Problem 1)
S2: Expose Filters in Browse Mode (Medium Impact, Low Effort)
Port Focus Mode's relationship type checkboxes and source system dropdown to Browse Mode.
Users can immediately:
- Uncheck
TRIGGERS_ONto hide table resources - Uncheck
OWNED_BYto simplify ownership edges - Filter to
source_system=entra_idto see only Azure entities
Effort: 2-3 hours (UI-only, filters already exist in focus mode)
S3: Adaptive Layout Spacing (Quick Win)
// Adjust nodesep based on node count per rank
const nodesPerRank = countNodesPerRank(entities);
const maxInRank = Math.max(...Object.values(nodesPerRank));
const adaptiveNodeSep = maxInRank > 30 ? 30 : maxInRank > 15 ? 50 : 80;
g.setGraph({ rankdir: "LR", nodesep: adaptiveNodeSep, ranksep: 200, align: "UL" });
Effort: 1 hour
S4: Collapsible Type Groups (Medium Effort, High UX Value)
When a rank has >N nodes of the same type, collapse them into a single summary node:
[identity x83] ──→ [role x3] ──→ [permission x4] ──→ [resource x20]
Clicking a group node expands it (re-layouts with that group's members).
Implementation:
- New
GroupNodecomponent in ReactFlow - Pre-layout pass: detect ranks with >10 same-type nodes
- Replace individual nodes with group summary
- Click handler: expand group, re-run layout
Effort: 8-12 hours
S5: Switch to TB (Top-Bottom) for Star Topologies
When the aspect ratio of the graph exceeds 1:5 (very tall, narrow), automatically switch from rankdir: "LR" to rankdir: "TB".
With TB layout:
- 83 identities spread horizontally across the top
- Roles/permissions below them
- Resources at the bottom
- Much more natural for star/fan-in topologies
Implementation:
const { width, height } = computeLayoutBounds(g);
if (height / width > 5) {
// Re-layout with TB
g.setGraph({ ...opts, rankdir: "TB" });
dagre.layout(g);
}
Effort: 2-3 hours (including re-layout logic)
S6: Node Count Warning + Auto-Suggest (Simple UX)
When entity count exceeds threshold (e.g., 50):
"126 entities in graph. Showing security-relevant only.
[Show all] [Use Focus Mode]"
Effort: 1 hour
Recommended Implementation Order
S1 (relevance default) ← Depends on Problem 1
↓
S2 (browse mode filters) ← Independent
↓
S6 (warning + auto-suggest) ← Quick win
↓
S3 (adaptive spacing) ← Quick win
↓
S5 (TB fallback for star topology) ← Medium win
↓
S4 (collapsible groups) ← Post-pilot polish
Critical insight: S1 alone reduces the graph from 126 to ~40 entities, which is perfectly readable with the current layout. The other solutions are defense-in-depth for when users expand filters to see all entities.
Combined Strategy
Phase A: Relevance Tiers (solves both problems)
-
Add
security_relevanceproperty in connector transformer:"active_threat": has external egress OR bound to SP with permissions"dormant_authority": has egress capability but 0 recent executions"internal_only": RG4 + unlinked + no egress + no executions
-
Platform Automations page defaults to
security_relevance != "internal_only" -
Graph browse mode defaults to same filter
-
"Show all automations" toggle reveals full inventory
Result: 126 entities → ~15-25 visible by default. Graph is readable. No false positives in default view.
Phase B: Graph Hardening (defense-in-depth)
- Port relationship type + source system filters to browse mode
- Add node count warning with "Use Focus Mode" suggestion
- Add adaptive nodesep calculation
- Add TB fallback for extreme aspect ratios
Phase C: Polish (post-pilot)
- Collapsible type groups
- Edge bundling for high fan-out nodes
Glossary Updates Needed
The glossary currently defines "Identity" but not "Automation" or "Autonomous Identity". Proposed additions:
| Term | Definition |
|---|---|
| Automation | A configured artifact in a source system that executes actions autonomously — business rules, scheduled jobs, Flow Designer flows, script includes. SecurityV0 tracks automations that have standing execution authority and can reach external systems. |
| Autonomous Identity | An identity entity with identitySubtype indicating it executes without human session — flow_designer_flow, business_rule, scheduled_job, system_execution. Stored as entity_type: "identity" with subtype in properties. |
| Security Relevance | Classification of an automation's threat surface: active_threat (external egress + active), dormant_authority (has capability but dormant), internal_only (no external reach, lowest priority). |
| Risk Group | Egress x origin matrix: RG1 (sensitive + LLM), RG2 (sensitive + external), RG3 (non-sensitive + external/LLM), RG4 (internal only), RG5 (unclassifiable). |
Appendix: Live Scan Data (2026-02-12)
Entity Type Distribution
| Type | Count | % |
|---|---|---|
| identity | 92 | 73% |
| resource | 20 | 16% |
| owner | 5 | 4% |
| permission | 4 | 3% |
| role | 3 | 2% |
| credential | 2 | 2% |
| Total | 126 |
Identity Subtype Distribution
| Subtype | Count | % of identities |
|---|---|---|
| flow_designer_flow | 83 | 90% |
| oauth_app | 3 | 3% |
| business_rule | 2 | 2% |
| system_execution | 2 | 2% |
| service_principal | 2 | 2% |
Security Relevance Distribution
| Category | Count | % |
|---|---|---|
| Active threat surface (RG1-3 + active) | 6 | 7% |
| Dormant authority (has capability, 0 exec) | 7 | 8% |
| Internal only (false positives) | 77 | 83% |
| Non-identity entities | 34 | n/a |
13 Security-Relevant Identities
| Name | Subtype | Exec/30d | Egress | Binding | RG |
|---|---|---|---|---|---|
| Auto-route identity tickets-Enta-no-own | business_rule | 0 | external | bound | RG3 |
| AzureGraphRouterNoOwner | system_execution | 0 | external | bound | RG3 |
| AzureGraphRouter | system_execution | 0 | external | bound | RG3 |
| Auto-route identity tickets via Entra | business_rule | 0 | external | bound | RG3 |
| sn-ticket-router (Graph) | oauth_app | 0 | - | bound | - |
| Azure Graph OAuth Client | oauth_app | 0 | - | bound | - |
| Azure OpenAI OAuth - sm | oauth_app | 0 | - | bound | - |
| AI Triage via Azure OpenAI (Catalog Trigger) | flow_designer_flow | 3 | llm | unlinked | RG3 |
| Change - Normal - Authorize | flow_designer_flow | 61 | none | unlinked | RG4 |
| Change - Normal - Assess | flow_designer_flow | 1 | none | unlinked | RG4 |
| Validate Environments Job | flow_designer_flow | 14 | none | unlinked | RG4 |
| Run SC Notifications | flow_designer_flow | 53 | none | unlinked | RG4 |
| Service Catalog Request | flow_designer_flow | 5 | none | unlinked | RG4 |