Skip to main content

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_inventory pre-filter shipped. Adds execution_mode and security_relevance properties to all automation nodes. Filters out flows matching: no external egress + no identity binding + no execution evidence. See 2026-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)

MetricCount%
Total entities126
Identity-type entities92100%
flow_designer_flow subtype8390% of identities
RG4 + 0 exec + unlinked + no egress7783% false positive
Security-relevant (has egress OR exec OR bound)1314%
Truly high-priority (RG1-RG3 + active)67%

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:

  1. Autonomous execution — runs without human session (scheduled, event-driven, record-change)
  2. Standing authority — has permissions that persist independently of any human
  3. External reach — can call external APIs, move data across system boundaries
  4. Active — has execution evidence (recent runs prove it's not just configured but running)
  5. Deterministic evidence — all of the above provable from first-party metadata/logs

An automation is NOT security-relevant (false positive) when:

  1. Human-initiated (requires click, catalog request, UI action)
  2. Internal-only (no egress, no cross-system data movement)
  3. Zero executions (configured but dormant)
  4. No identity binding (no SP/OAuth, no standing credential)
  5. System-default (out-of-box ServiceNow workflow, not custom)

Example: "Procurement Process Flow - DEFAULT"

SignalValueVerdict
Executions (30d)0Not active
Egress categorynoneNo external reach
Binding statusunlinkedNo standing NHI credential
Run-as"User who initiates the session"Human context, not autonomous
Risk groupRG4Lowest priority
Resources accessible0No 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_category in ["external", "llm"] (RG1-RG3)
  • identity_binding_status == "bound" (has RUNS_AS edge to SP/OAuth)
  • execution_count_30d > 0 AND 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_category in ["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_tier property 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:

  1. 83 identity nodes (flows) all in the same Dagre rank (column 0)
  2. Each node takes 60px + 80px spacing = 140px vertical
  3. 83 nodes x 140px = 11,620px vertical extent
  4. All edges converge to 3-5 shared role/permission nodes
  5. 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

FilterBrowse ModeFocus Mode
Entity type toggleYesYes
Has findings onlyYesYes
Relationship type filterNoYes
Source system filterNoYes
Depth controlNoYes (1-3)
Relevance/tier filterNoNo

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_ON to hide table resources
  • Uncheck OWNED_BY to simplify ownership edges
  • Filter to source_system=entra_id to 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 GroupNode component 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

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)

  1. Add security_relevance property 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
  2. Platform Automations page defaults to security_relevance != "internal_only"

  3. Graph browse mode defaults to same filter

  4. "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)

  1. Port relationship type + source system filters to browse mode
  2. Add node count warning with "Use Focus Mode" suggestion
  3. Add adaptive nodesep calculation
  4. Add TB fallback for extreme aspect ratios

Phase C: Polish (post-pilot)

  1. Collapsible type groups
  2. Edge bundling for high fan-out nodes

Glossary Updates Needed

The glossary currently defines "Identity" but not "Automation" or "Autonomous Identity". Proposed additions:

TermDefinition
AutomationA 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 IdentityAn 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 RelevanceClassification 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 GroupEgress 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

TypeCount%
identity9273%
resource2016%
owner54%
permission43%
role32%
credential22%
Total126

Identity Subtype Distribution

SubtypeCount% of identities
flow_designer_flow8390%
oauth_app33%
business_rule22%
system_execution22%
service_principal22%

Security Relevance Distribution

CategoryCount%
Active threat surface (RG1-3 + active)67%
Dormant authority (has capability, 0 exec)78%
Internal only (false positives)7783%
Non-identity entities34n/a

13 Security-Relevant Identities

NameSubtypeExec/30dEgressBindingRG
Auto-route identity tickets-Enta-no-ownbusiness_rule0externalboundRG3
AzureGraphRouterNoOwnersystem_execution0externalboundRG3
AzureGraphRoutersystem_execution0externalboundRG3
Auto-route identity tickets via Entrabusiness_rule0externalboundRG3
sn-ticket-router (Graph)oauth_app0-bound-
Azure Graph OAuth Clientoauth_app0-bound-
Azure OpenAI OAuth - smoauth_app0-bound-
AI Triage via Azure OpenAI (Catalog Trigger)flow_designer_flow3llmunlinkedRG3
Change - Normal - Authorizeflow_designer_flow61noneunlinkedRG4
Change - Normal - Assessflow_designer_flow1noneunlinkedRG4
Validate Environments Jobflow_designer_flow14noneunlinkedRG4
Run SC Notificationsflow_designer_flow53noneunlinkedRG4
Service Catalog Requestflow_designer_flow5noneunlinkedRG4