159 lines
7.6 KiB
Markdown
159 lines
7.6 KiB
Markdown
# 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.
|
|
|
|
**Definitions**:
|
|
- **Blast radius**: All resources directly or transitively affected by a change to a given item (outbound edges up to depth 2).
|
|
- **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.)
|
|
- `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 paginated, ordered by `created_at DESC`.
|
|
|
|
- **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.
|
|
|
|
- **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
|
|
- Record warning in sync run metadata: `{type: 'unsupported_reference', policy_id, raw_ref, reason}`
|
|
- Sync run continues without failure
|
|
|
|
## Graph Traversal & Cycles
|
|
|
|
- UI blast radius view is limited to depth ≤ 2 (direct neighbors and their neighbors).
|
|
- Traversal uses visited-node tracking to prevent revisiting nodes (cycle break); cycles are implicitly handled by not re-visiting.
|
|
- No special UI cycle annotation in MVP; future work may visualize cycles explicitly.
|
|
|
|
## Success Criteria
|
|
|
|
- **SC1: Blast radius determination**
|
|
Admins can determine prerequisites (inbound edges) and blast radius (outbound edges, depth ≤2) 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, 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.
|
|
|
|
## Related Specs
|
|
|
|
- Program: `specs/039-inventory-program/spec.md`
|
|
- Core: `specs/040-inventory-core/spec.md`
|