TenantAtlas/specs/042-inventory-dependencies-graph/spec.md
2026-01-10 00:17:15 +01:00

8.8 KiB

Feature Specification: Inventory Dependencies Graph

Feature Branch: feat/042-inventory-dependencies-graph
Created: 2026-01-07
Status: Draft

Purpose

Represent and surface dependency relationships between inventory items and foundational Intune objects so admins can understand blast radius and prerequisites.

MVP shows direct inbound/outbound edges only; depth > 1 traversal is out of scope for this iteration.

Clarifications

Session 2026-01-10

  • Q: Should FR3 be paginated or limit-only for MVP? → A: Limit-only (no pagination).
  • Q: Where should unknown/unsupported reference warnings be persisted? → A: On the inventory sync run record (e.g., InventorySyncRun.error_context.warnings[]).
  • Q: For unknown assignment target shapes, should we create a missing edge or warning-only? → A: Warning-only (no edge created).
  • Q: Should foundation_object edges always store metadata.foundation_type? → A: Yes (required).
  • Q: Should the UI show 50 edges total or 50 per direction? → A: 50 per direction (up to 100 total when showing both directions).

Definitions:

  • Blast radius: All resources directly affected by a change to a given item (outbound edges only; no transitive traversal in MVP).
  • Prerequisite: A hard dependency required for an item to function; missing prerequisites are explicitly surfaced.
  • Inbound edge: A relationship pointing TO this item (e.g., "Policy A is assigned to Group X" → Group X has inbound edge from Policy A).
  • Outbound edge: A relationship pointing FROM this item (e.g., "Policy A is scoped by ScopeTag Y" → Policy A has outbound edge to ScopeTag Y).

User Scenarios & Testing

Scenario 1: View dependencies for an item

  • Given an inventory item
  • When the user opens its dependencies view
  • Then they can see inbound and outbound relationships (e.g., “uses”, “assigned to”, “scoped by”)

Scenario 2: Identify missing prerequisites

  • Given an item references a prerequisite object not present in inventory
  • When the user views dependencies
  • Then missing prerequisites are clearly indicated (red badge, "Missing" label, tooltip with last-known displayName if available)

Scenario 3: Zero dependencies

  • Given an item has no inbound or outbound edges
  • When the user opens dependencies view
  • Then a "No dependencies found" message is shown

Scenario 4: Filter dependencies by relationship type

  • Given multiple relationship types exist
  • When the user filters by relationship type (single-select dropdown, default: "All")
  • Then only matching edges are shown (empty selection = all edges visible)

Scenario 5: Only missing prerequisites

  • Given an item where all referenced targets are unresolved (no matching inventory or foundation objects)
  • When the user opens the dependencies view and selects "Outbound" or "All"
  • Then all shown edges are annotated as "Missing" with a red badge and tooltip; filtering still works and zero resolvable targets do not error

Functional Requirements

  • FR1: Relationship taxonomy
    Define a normalized set of relationship types covering inventory→inventory and inventory→foundation edges.
    Supported types (MVP):

    • assigned_to (Policy → AAD Group)
    • scoped_by (Policy → Scope Tag)
    • targets (Update Policy → Device Category, conditional logic)
    • depends_on (Generic prerequisite, e.g., Compliance Policy referenced by Conditional Access)

    Each type has:

    • name (string, e.g., "assigned_to")
    • display_label (string, e.g., "Assigned to")
    • directionality (enum: outbound, inbound, bidirectional)
    • description (brief explanation)
  • FR2: Dependency edge storage
    Store edges in an inventory_links table with fields:

  • id (PK)

  • tenant_id (FK, indexed)

  • source_type (string: inventory_item, foundation_object)

  • source_id (UUID or stable ref)

  • target_type (string: inventory_item, foundation_object, missing)

  • target_id (UUID or stable ref, nullable if missing)

  • relationship_type (FK to taxonomy or enum)

  • metadata (JSONB, optional: last_known_name, raw_ref, etc.; for target_type='foundation_object', metadata.foundation_type is required)

  • created_at, updated_at

In-scope foundation object types (MVP):

  • AAD Groups (aad_group)
  • Scope Tags (scope_tag)
  • Device Categories (device_category)

Out-of-scope foundation types (for this iteration): Conditional Access Policies, Compliance Policies as foundation nodes (only as inventory items).

  • FR3: Query inbound/outbound edges
    Provide service methods:

    • getOutboundEdges(item_id, relationship_type?, limit=50) → returns edges where item is source
    • getInboundEdges(item_id, relationship_type?, limit=50) → returns edges where item is target

    Both return up to limit edges, ordered by created_at DESC.

    UI supports filtering by relationship_type via a single-select dropdown (default: "All"; empty selection behaves as "All").

  • FR4: Missing prerequisites
    When a target reference cannot be resolved:

    • Create edge with target_type='missing', target_id=null
    • Store metadata.last_known_name and metadata.raw_ref if available
    • UI displays "Missing" badge + tooltip

    No separate "deleted" or "archived" state in core inventory; missing is purely an edge property.

    Unknown/unsupported reference shapes do not create edges; they are handled via warnings (see NFR2).

  • FR5: Tenant scoping and access control

    • All edges filtered by tenant_id matching Tenant::current()
    • Read access: any authenticated tenant user
    • No cross-tenant queries allowed (enforced at query builder level)

Non-Functional Requirements

  • NFR1: Idempotency
    Dependency extraction must be idempotent:

    • Unique key: (tenant_id, source_type, source_id, target_type, target_id, relationship_type)
    • On re-run: upsert (update updated_at, replace metadata if changed)
    • Orphan edges (source/target no longer in inventory) are NOT auto-deleted; cleanup is manual or scheduled separately
  • NFR2: Graceful unknown-reference handling
    If an unknown/unsupported reference shape is encountered:

    • Log warning with severity info (not error)
    • Do NOT create an edge for unsupported types (including unknown assignment target shapes)
    • Record warning in sync run metadata at InventorySyncRun.error_context.warnings[] with shape: {type: 'unsupported_reference', policy_id, raw_ref, reason}
    • Sync run continues without failure

Graph Traversal & Cycles (Out of Scope for MVP)

  • Depth > 1 traversal (transitive “blast radius”) is out of scope for this iteration.
  • The UI shows only direct inbound/outbound edges.
  • Future work may add depth-capped traversal with cycle handling and explicit cycle visualization.

Success Criteria

  • SC1: Blast radius determination
    Admins can determine prerequisites (inbound edges) and blast radius (outbound edges; direct only) for any item in under 2 minutes:

    • Measured from: clicking "View Dependencies" on an item detail page
    • To: able to answer "What would break if I delete this?" and "What does this depend on?"
    • Acceptance: <2s page load, ≤50 edges per direction shown initially (≤100 total when showing both directions), clear visual grouping by relationship type
  • SC2: Deterministic output
    For supported relationship types, dependency edges are consistent across re-runs:

    • Given identical inventory state (same items, same Graph API responses)
    • Edge set equality: same (source, target, relationship_type) tuples (order-independent)
    • Acceptance: automated test re-runs extraction twice on fixed test data; assert edge sets match (ignoring updated_at)

Security & Privacy

  • All data is tenant-scoped; no cross-tenant queries or joins.
  • Foundation object visibility:
    • Display name shown only if available from tenant-authorized sources (inventory metadata or prior sync payloads).
    • If not available, show a masked or abbreviated identifier (e.g., first 6 characters of ID) with no external lookup.
  • Stored metadata for edges must avoid PII beyond display names surfaced by Graph within the tenant; raw references may be stored but not enriched from outside the tenant scope.

Traceability

  • FR1 (taxonomy) → SC2 (deterministic types), tests: unit taxonomy load/assert
  • FR2 (storage) → SC2 (edge equality), tests: feature upsert and equality
  • FR3 (queries) → SC1 (answer in <2 min), tests: service returns inbound/outbound within limits
  • FR4 (missing) → SC1 (clear prerequisite view), tests: feature missing badge/tooltip
  • FR5 (tenant scope) → SC1/SC2 (correct data, deterministic set), tests: tenant isolation

Out of Scope

  • Automatic remediation.
  • Cross-tenant dependency graphs.
  • Program: specs/039-inventory-program/spec.md
  • Core: specs/040-inventory-core/spec.md