Dieses PR liefert den Inventory Dependencies Graph end-to-end: Abhängigkeiten (Edges) werden aus Inventory-Sync-Daten extrahiert, tenant-sicher gespeichert und in der Inventory Item Detailansicht angezeigt. Ziel: Admins können Prerequisites + Blast Radius (direct) schnell erkennen, ohne Snapshot/Restore anzufassen. ⸻ Was ist drin? Dependency Graph (Edges) • inventory_links Schema + Indizes + idempotentes Upsert (Unique Key) • Relationship Types (u.a.): • assigned_to_include, assigned_to_exclude • uses_assignment_filter • scoped_by_scope_tag • UI: Inventory Item → Dependencies Section • Direction Filter: All / Inbound / Outbound • Relationship Filter: All + spezifische Relationship Types • Missing-Badge + sicheres Tooltip (safe subset) Safety / Observability • Unknown/unsupported Shapes erzeugen keine Edges, sondern: • Warning in InventorySyncRun.error_context.warnings[] • optional info-log (ohne Secrets) • Limit-only Semantik (MVP): bis zu 50 Edges pro Richtung (max 100 bei “All”) • Blast Radius in MVP = direct only (kein depth>1 traversal) Name Resolution (lokal, ohne Entra Calls) • Resolver/DTO Layer für deterministische Labels (kein “Unknown” mehr) • Auflösung aus lokaler DB nur für Foundations, wenn vorhanden: • scope_tag → roleScopeTag • assignment_filter → assignmentFilter • aad_group bleibt bewusst external ref: “Group (external): …” (keine Graph/Entra Lookups im UI) • Zentraler FoundationTypeMap als Source-of-Truth (keine Hardcodings) ⸻ Out of Scope / Follow-up • Entra Group Name Resolution (braucht eigenes “Group Inventory” Modul + Permissions) • Foundations als Inventory Items / Coverage Tab (Scope Tags / Assignment Filters sichtbar & syncbar) → folgt als separater PR (Inventory Core/UI), damit 042 sauber “Edges-only” bleibt. ⸻ Tests / Verifikation • Targeted Pest Tests (Unit + Feature + UI smoke) für: • deterministische Edge-Erzeugung + idempotent upsert • tenant isolation (UI/Query) • warnings auf Run Record • resolver/name rendering + links (wo möglich) • pint --dirty ausgeführt ⸻ Manual QA (UI) 1. Inventory Sync Run mit include_dependencies=true starten 2. Inventory Item öffnen → Dependencies prüfen: • include/exclude + filter + scoped_by sichtbar (wenn vorhanden) • Relationship/Direction Filter funktionieren • keine “Unknown” Labels mehr, sondern deterministische Labels Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.local> Reviewed-on: #50
9.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_objectedges always storemetadata.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).
Session 2026-01-10 (042.2)
- Q: Should the Dependencies UI do Entra/Graph lookups to resolve names (e.g., Groups)? → A: No. UI resolution is DB-only.
- Q: How should the UI avoid "Unknown" targets long-term? → A: Render a stable, typed DTO per edge target via a resolver layer (batch queries, no N+1). 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 6 (042.2): Resolve target names when available
- Given dependency edges to foundation objects (scope tags, assignment filters, groups)
- When the user views dependencies
- Then each edge target is labelled deterministically (no "Unknown")
- And names are resolved only when the target exists in the local DB (no UI Graph lookups)
- And external references (AAD groups) are rendered as external refs without links
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) (legacy/general)assigned_to_include(Policy → AAD Group; include assignment)assigned_to_exclude(Policy → AAD Group; exclude assignment)uses_assignment_filter(Policy → Assignment Filter; metadatafilter_mode=include|exclude)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 aninventory_linkstable 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.; fortarget_type='foundation_object',metadata.foundation_typeis required) -
created_at,updated_at
In-scope foundation object types (MVP):
- AAD Groups (
aad_group) - Scope Tags (
scope_tag) - Device Categories (
device_category) - Assignment Filters (
assignment_filter)
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 sourcegetInboundEdges(item_id, relationship_type?, limit=50)→ returns edges where item is target
Both return up to
limitedges, ordered bycreated_at DESC.UI supports filtering by
relationship_typevia 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_nameandmetadata.raw_refif 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).
- Create edge with
-
FR5: Tenant scoping and access control
- All edges filtered by
tenant_idmatchingTenant::current() - Read access: any authenticated tenant user
- No cross-tenant queries allowed (enforced at query builder level)
- All edges filtered by
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, replacemetadataif changed) - Orphan edges (source/target no longer in inventory) are NOT auto-deleted; cleanup is manual or scheduled separately
- Unique key:
-
NFR2: Graceful unknown-reference handling
If an unknown/unsupported reference shape is encountered:- Log warning with severity
info(noterror) - 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
- Log warning with severity
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.
Related Specs
- Program:
specs/039-inventory-program/spec.md - Core:
specs/040-inventory-core/spec.md