Compare commits
6 Commits
dev
...
feat/018-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5743b9fbb | ||
|
|
b94377db8e | ||
|
|
720f13a866 | ||
|
|
a7d715c89e | ||
|
|
cb5cf9b3bd | ||
|
|
662b0e0aa8 |
@ -1,50 +1,35 @@
|
|||||||
# [PROJECT_NAME] Constitution
|
# TenantPilot Constitution
|
||||||
<!-- Example: Spec Constitution, TaskFlow Constitution, etc. -->
|
|
||||||
|
|
||||||
## Core Principles
|
## Core Principles
|
||||||
|
|
||||||
### [PRINCIPLE_1_NAME]
|
### Safety-First Restore
|
||||||
<!-- Example: I. Library-First -->
|
- Any destructive action MUST support preview/dry-run, explicit confirmation, and a clear pre-execution summary.
|
||||||
[PRINCIPLE_1_DESCRIPTION]
|
- High-risk policy types default to `preview-only` restore unless explicitly enabled by a feature spec + tests + checklist.
|
||||||
<!-- Example: Every feature starts as a standalone library; Libraries must be self-contained, independently testable, documented; Clear purpose required - no organizational-only libraries -->
|
- Restore must be defensive: validate inputs, detect conflicts, allow selective restore, and record outcomes per item.
|
||||||
|
|
||||||
### [PRINCIPLE_2_NAME]
|
### Auditability & Tenant Isolation
|
||||||
<!-- Example: II. CLI Interface -->
|
- Every operation is tenant-scoped and MUST write an audit log entry (no secrets, no tokens).
|
||||||
[PRINCIPLE_2_DESCRIPTION]
|
- Snapshots are immutable JSONB and MUST remain reproducible (who/when/what/source tenant).
|
||||||
<!-- Example: Every library exposes functionality via CLI; Text in/out protocol: stdin/args → stdout, errors → stderr; Support JSON + human-readable formats -->
|
|
||||||
|
|
||||||
### [PRINCIPLE_3_NAME]
|
### Graph Abstraction & Contracts
|
||||||
<!-- Example: III. Test-First (NON-NEGOTIABLE) -->
|
- All Microsoft Graph calls MUST go through `GraphClientInterface`.
|
||||||
[PRINCIPLE_3_DESCRIPTION]
|
- Contract assumptions are config-driven (`config/graph_contracts.php`); do not hardcode endpoints in feature code.
|
||||||
<!-- Example: TDD mandatory: Tests written → User approved → Tests fail → Then implement; Red-Green-Refactor cycle strictly enforced -->
|
- Unknown/missing policy types MUST fail safe (preview-only / no Graph calls) rather than calling `deviceManagement/{type}`.
|
||||||
|
|
||||||
### [PRINCIPLE_4_NAME]
|
### Least Privilege
|
||||||
<!-- Example: IV. Integration Testing -->
|
- Prefer least-privilege roles/scopes; surface warnings when higher privileges are selected.
|
||||||
[PRINCIPLE_4_DESCRIPTION]
|
- Never store secrets in code/config; never log credentials or tokens.
|
||||||
<!-- Example: Focus areas requiring integration tests: New library contract tests, Contract changes, Inter-service communication, Shared schemas -->
|
|
||||||
|
|
||||||
### [PRINCIPLE_5_NAME]
|
### Spec-First Workflow
|
||||||
<!-- Example: V. Observability, VI. Versioning & Breaking Changes, VII. Simplicity -->
|
- For any feature that changes runtime behavior, include or update `specs/<NNN>-<slug>/` with `spec.md`, `plan.md`, `tasks.md`, and `checklists/requirements.md`.
|
||||||
[PRINCIPLE_5_DESCRIPTION]
|
- New work branches from `dev` using `feat/<NNN>-<slug>` (spec + code in the same PR).
|
||||||
<!-- Example: Text I/O ensures debuggability; Structured logging required; Or: MAJOR.MINOR.BUILD format; Or: Start simple, YAGNI principles -->
|
|
||||||
|
|
||||||
## [SECTION_2_NAME]
|
## Quality Gates
|
||||||
<!-- Example: Additional Constraints, Security Requirements, Performance Standards, etc. -->
|
- Changes MUST be programmatically tested (Pest) and run via targeted `php artisan test ...`.
|
||||||
|
- Run `./vendor/bin/pint --dirty` before finalizing.
|
||||||
[SECTION_2_CONTENT]
|
|
||||||
<!-- Example: Technology stack requirements, compliance standards, deployment policies, etc. -->
|
|
||||||
|
|
||||||
## [SECTION_3_NAME]
|
|
||||||
<!-- Example: Development Workflow, Review Process, Quality Gates, etc. -->
|
|
||||||
|
|
||||||
[SECTION_3_CONTENT]
|
|
||||||
<!-- Example: Code review requirements, testing gates, deployment approval process, etc. -->
|
|
||||||
|
|
||||||
## Governance
|
## Governance
|
||||||
<!-- Example: Constitution supersedes all other practices; Amendments require documentation, approval, migration plan -->
|
- This constitution applies across the repo. Feature specs may add stricter constraints but not weaker ones.
|
||||||
|
- Restore semantics changes require: spec update, checklist update, and tests proving safety.
|
||||||
|
|
||||||
[GOVERNANCE_RULES]
|
**Version**: 1.0.0 | **Ratified**: 2026-01-03 | **Last Amended**: 2026-01-03
|
||||||
<!-- Example: All PRs/reviews must verify compliance; Complexity must be justified; Use [GUIDANCE_FILE] for runtime development guidance -->
|
|
||||||
|
|
||||||
**Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE]
|
|
||||||
<!-- Example: Version: 2.1.1 | Ratified: 2025-06-13 | Last Amended: 2025-07-16 -->
|
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
# Implementation Plan: TenantPilot v1
|
# Implementation Plan: TenantPilot v1
|
||||||
|
|
||||||
**Branch**: `tenantpilot-v1`
|
**Branch**: `dev`
|
||||||
**Date**: 2025-12-12
|
**Date**: 2026-01-03
|
||||||
**Spec Source**: `.specify/spec.md` (scope/restore matrix unchanged)
|
**Spec Source**: `.specify/spec.md` (scope/restore matrix is config-driven)
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
TenantPilot v1 already delivers tenant-scoped Intune inventory, immutable backups, version history with diffs, defensive restore flows, tenant setup, permissions/health, settings normalization/display, and Highlander enforcement. Remaining priority work is the delegated Intune RBAC onboarding wizard (US7) and afterwards the Graph Contract Registry & Drift Guard (US8). All Graph calls stay behind the abstraction with audit logging; snapshots remain JSONB with safety gates (preview-only for high-risk types).
|
TenantPilot v1 delivers tenant-scoped Intune inventory, immutable backups, version history with diffs, defensive restore flows, tenant setup, permissions/health, settings normalization/display, Highlander enforcement, the delegated RBAC onboarding wizard (US7), and the Graph Contract Registry & Drift Guard (US8). All Graph calls stay behind the abstraction with audit logging; snapshots remain JSONB with safety gates (preview-only for high-risk types).
|
||||||
|
|
||||||
## Status Snapshot (tasks.md is source of truth)
|
## Status Snapshot (tasks.md is source of truth)
|
||||||
- **Done**: US1 inventory, US2 backups, US3 versions/diffs, US4 restore preview/exec, scope config, soft-deletes/housekeeping, Highlander single current tenant, tenant setup & verify (US6), permissions/health overview (US6), table ActionGroup UX, settings normalization/display (US1b), Dokploy/Sail runbooks.
|
- **Done**: Phases 1–15 (US1–US8, Settings Catalog hydration/display, restore rerun, Highlander, permissions/health, housekeeping/UX, ops).
|
||||||
- **Next up**: **US7** Intune RBAC onboarding wizard (delegated, synchronous Filament flow).
|
- **Open**: T167 (optional) CLI/Job for CHECK/REPORT only (no grant).
|
||||||
- **Upcoming**: **US8** Graph Contract Registry & Drift Guard (contract registry, type-family handling, verification command, fallback strategies).
|
- **Next up**: Feature 018 (Driver Updates / WUfB add-on) in `specs/018-driver-updates-wufb/`.
|
||||||
|
|
||||||
## Technical Baseline
|
## Technical Baseline
|
||||||
- Laravel 12, Filament 4, PHP 8.4; Sail-first with PostgreSQL.
|
- Laravel 12, Filament 4, PHP 8.4; Sail-first with PostgreSQL.
|
||||||
@ -28,10 +28,12 @@ ## Completed Workstreams (no new action needed)
|
|||||||
- **US6 Tenant Setup & Highlander (Phases 8 & 12)**: Tenant CRUD/verify, INTUNE_TENANT_ID override, `is_current` unique enforcement, “Make current” action, block deactivated tenants.
|
- **US6 Tenant Setup & Highlander (Phases 8 & 12)**: Tenant CRUD/verify, INTUNE_TENANT_ID override, `is_current` unique enforcement, “Make current” action, block deactivated tenants.
|
||||||
- **US6 Permissions/Health (Phase 9)**: Required permissions list, compare/check service, Verify action updates status and audit, permissions panel in Tenant detail.
|
- **US6 Permissions/Health (Phase 9)**: Required permissions list, compare/check service, Verify action updates status and audit, permissions panel in Tenant detail.
|
||||||
- **US1b Settings Display (Phase 13)**: PolicyNormalizer + SnapshotValidator, warnings for malformed snapshots, normalized settings and pretty JSON on policy/version detail, list badges, README section.
|
- **US1b Settings Display (Phase 13)**: PolicyNormalizer + SnapshotValidator, warnings for malformed snapshots, normalized settings and pretty JSON on policy/version detail, list badges, README section.
|
||||||
|
- **US7 RBAC Wizard (Phase 14)**: Delegated, synchronous onboarding wizard with post-verify canary checks and audit trail.
|
||||||
|
- **US8 Graph Contracts & Drift Guard (Phase 15)**: Config-driven contract registry, type-family handling, capability downgrade fallbacks, and a drift-check command.
|
||||||
- **Housekeeping/UX (Phases 10–12)**: Soft/force deletes for tenants/backups/versions/restore runs with guards; table actions in ActionGroup per UX guideline.
|
- **Housekeeping/UX (Phases 10–12)**: Soft/force deletes for tenants/backups/versions/restore runs with guards; table actions in ActionGroup per UX guideline.
|
||||||
- **Ops (Phase 7)**: Sail runbook and Dokploy staging→prod guidance captured.
|
- **Ops (Phase 7)**: Sail runbook and Dokploy staging→prod guidance captured.
|
||||||
|
|
||||||
## Execution Plan: US7 Intune RBAC Onboarding Wizard (Phase 14)
|
## Completed: US7 Intune RBAC Onboarding Wizard (Phase 14)
|
||||||
|
|
||||||
- Objectives: deliver delegated, tenant-scoped wizard that safely converges the Intune RBAC state for the configured service principal; fully audited, idempotent, least-privilege by default.
|
- Objectives: deliver delegated, tenant-scoped wizard that safely converges the Intune RBAC state for the configured service principal; fully audited, idempotent, least-privilege by default.
|
||||||
- Scope alignment: FR-023–FR-030, constitution (Safety-First, Auditability, Tenant-Aware, Graph Abstraction). No secret/token persistence; delegated tokens stay request-local and are not stored in DB/cache.
|
- Scope alignment: FR-023–FR-030, constitution (Safety-First, Auditability, Tenant-Aware, Graph Abstraction). No secret/token persistence; delegated tokens stay request-local and are not stored in DB/cache.
|
||||||
@ -56,7 +58,7 @@ ## Execution Plan: US7 Intune RBAC Onboarding Wizard (Phase 14)
|
|||||||
- Health integration: Verify reflects RBAC status and prompts to run wizard when missing.
|
- Health integration: Verify reflects RBAC status and prompts to run wizard when missing.
|
||||||
- Deployment/ops: no new env vars; ensure migrations for tenant RBAC columns are applied; run targeted tests `php artisan test tests/Unit/RbacOnboardingServiceTest.php tests/Feature/Filament/TenantRbacWizardTest.php`; Pint on touched files.
|
- Deployment/ops: no new env vars; ensure migrations for tenant RBAC columns are applied; run targeted tests `php artisan test tests/Unit/RbacOnboardingServiceTest.php tests/Feature/Filament/TenantRbacWizardTest.php`; Pint on touched files.
|
||||||
|
|
||||||
## Upcoming: US8 Graph Contract Registry & Drift Guard (Phase 15)
|
## Completed: US8 Graph Contract Registry & Drift Guard (Phase 15)
|
||||||
|
|
||||||
- Objectives: centralize Graph contract assumptions per supported type/endpoint and provide drift detection + safe fallbacks so preview/restore remain stable on Graph shape/capability changes.
|
- Objectives: centralize Graph contract assumptions per supported type/endpoint and provide drift detection + safe fallbacks so preview/restore remain stable on Graph shape/capability changes.
|
||||||
- Scope alignment: FR-031–FR-034 (spec), constitution (Safety-First, Auditability, Graph Abstraction, Tenant-Aware).
|
- Scope alignment: FR-031–FR-034 (spec), constitution (Safety-First, Auditability, Graph Abstraction, Tenant-Aware).
|
||||||
@ -74,7 +76,7 @@ ## Upcoming: US8 Graph Contract Registry & Drift Guard (Phase 15)
|
|||||||
- Testing outline: unit for registry lookups/type-family matching/fallback selection; integration/Pest to simulate capability errors and ensure downgrade path + correct routing for derived types.
|
- Testing outline: unit for registry lookups/type-family matching/fallback selection; integration/Pest to simulate capability errors and ensure downgrade path + correct routing for derived types.
|
||||||
|
|
||||||
## Testing & Quality Gates
|
## Testing & Quality Gates
|
||||||
- Continue using targeted Pest runs per change set; add/extend tests for US7 wizard now, and for US8 contracts when implemented.
|
- Continue using targeted Pest runs per change set; add/extend tests when RBAC/contract behavior changes.
|
||||||
- Run Pint on touched files before finalizing.
|
- Run Pint on touched files before finalizing.
|
||||||
- Maintain tenant isolation, audit logging, and restore safety gates; validate snapshot shape and type-family compatibility prior to restore execution.
|
- Maintain tenant isolation, audit logging, and restore safety gates; validate snapshot shape and type-family compatibility prior to restore execution.
|
||||||
|
|
||||||
@ -83,6 +85,6 @@ ### Restore Safety Gate
|
|||||||
- Restore preview MAY still render details + warnings for out-of-family snapshots, but MUST NOT offer an apply action.
|
- Restore preview MAY still render details + warnings for out-of-family snapshots, but MUST NOT offer an apply action.
|
||||||
|
|
||||||
## Coordination
|
## Coordination
|
||||||
- Update `.specify/tasks.md` to reflect progress on US7 wizard and future US8 contract tasks; no new entities or scope changes introduced here.
|
- Keep `.specify/tasks.md` and per-feature specs under `specs/` aligned with implementation changes.
|
||||||
- Stage validation required before production for any migration or restore-impacting change.
|
- Stage validation required before production for any migration or restore-impacting change.
|
||||||
- Keep Graph integration behind abstraction; no secrets in logs; follow existing UX patterns (ActionGroup, warnings for risky ops).
|
- Keep Graph integration behind abstraction; no secrets in logs; follow existing UX patterns (ActionGroup, warnings for risky ops).
|
||||||
164
.specify/spec.md
164
.specify/spec.md
@ -1,20 +1,50 @@
|
|||||||
# Feature Specification: TenantPilot v1
|
# Feature Specification: TenantPilot v1
|
||||||
|
|
||||||
**Feature Branch**: `tenantpilot-v1`
|
**Feature Branch**: `dev`
|
||||||
**Created**: 2025-12-10
|
**Created**: 2025-12-10
|
||||||
**Status**: Draft
|
**Status**: Active
|
||||||
|
**Last Updated**: 2026-01-03
|
||||||
**Input**: TenantPilot v1 scope covering Intune configuration inventory (config, compliance, scripts, apps, conditional access, endpoint security, enrollment/autopilot, RBAC), backup, version history, and defensive restore for Intune administrators.
|
**Input**: TenantPilot v1 scope covering Intune configuration inventory (config, compliance, scripts, apps, conditional access, endpoint security, enrollment/autopilot, RBAC), backup, version history, and defensive restore for Intune administrators.
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
scope:
|
scope:
|
||||||
description: "v1 muss folgende Intune-Objekttypen inventarisieren, sichern und – je nach Risikoklasse – wiederherstellen können."
|
description: "v1 muss folgende Intune-Objekttypen inventarisieren, sichern und – je nach Risikoklasse – wiederherstellen können. Single Source of Truth: config/tenantpilot.php + config/graph_contracts.php."
|
||||||
supported_types:
|
supported_types:
|
||||||
- key: deviceConfiguration
|
- key: deviceConfiguration
|
||||||
name: "Device Configuration"
|
name: "Device Configuration"
|
||||||
graph_resource: "deviceManagement/deviceConfigurations"
|
graph_resource: "deviceManagement/deviceConfigurations"
|
||||||
notes: "Inklusive Custom OMA-URI, Administrative Templates und Settings Catalog."
|
filter: "not isof('microsoft.graph.windowsUpdateForBusinessConfiguration')"
|
||||||
|
notes: "Standard Device Config inkl. Custom OMA-URI; excludes WUfB Update Rings."
|
||||||
|
|
||||||
|
- key: groupPolicyConfiguration
|
||||||
|
name: "Administrative Templates"
|
||||||
|
graph_resource: "deviceManagement/groupPolicyConfigurations"
|
||||||
|
notes: "Administrative Templates (Group Policy)."
|
||||||
|
|
||||||
|
- key: settingsCatalogPolicy
|
||||||
|
name: "Settings Catalog Policy"
|
||||||
|
graph_resource: "deviceManagement/configurationPolicies"
|
||||||
|
notes: "Settings Catalog policies; settings are hydrated from the /settings subresource."
|
||||||
|
|
||||||
|
- key: windowsUpdateRing
|
||||||
|
name: "Software Update Ring"
|
||||||
|
graph_resource: "deviceManagement/deviceConfigurations"
|
||||||
|
filter: "isof('microsoft.graph.windowsUpdateForBusinessConfiguration')"
|
||||||
|
notes: "Windows Update for Business (WUfB) update rings."
|
||||||
|
|
||||||
|
- key: windowsFeatureUpdateProfile
|
||||||
|
name: "Feature Updates (Windows)"
|
||||||
|
graph_resource: "deviceManagement/windowsFeatureUpdateProfiles"
|
||||||
|
|
||||||
|
- key: windowsQualityUpdateProfile
|
||||||
|
name: "Quality Updates (Windows)"
|
||||||
|
graph_resource: "deviceManagement/windowsQualityUpdateProfiles"
|
||||||
|
|
||||||
|
- key: windowsDriverUpdateProfile
|
||||||
|
name: "Driver Updates (Windows)"
|
||||||
|
graph_resource: "deviceManagement/windowsDriverUpdateProfiles"
|
||||||
|
|
||||||
- key: deviceCompliancePolicy
|
- key: deviceCompliancePolicy
|
||||||
name: "Device Compliance"
|
name: "Device Compliance"
|
||||||
@ -25,6 +55,16 @@ ## Scope
|
|||||||
graph_resource: "deviceAppManagement/managedAppPolicies"
|
graph_resource: "deviceAppManagement/managedAppPolicies"
|
||||||
notes: "iOS und Android Managed App Protection."
|
notes: "iOS und Android Managed App Protection."
|
||||||
|
|
||||||
|
- key: mamAppConfiguration
|
||||||
|
name: "App Configuration (MAM)"
|
||||||
|
graph_resource: "deviceAppManagement/targetedManagedAppConfigurations"
|
||||||
|
notes: "App configuration targeting managed apps (MAM)."
|
||||||
|
|
||||||
|
- key: managedDeviceAppConfiguration
|
||||||
|
name: "App Configuration (Device)"
|
||||||
|
graph_resource: "deviceAppManagement/mobileAppConfigurations"
|
||||||
|
notes: "Managed device app configuration profiles."
|
||||||
|
|
||||||
- key: conditionalAccessPolicy
|
- key: conditionalAccessPolicy
|
||||||
name: "Conditional Access"
|
name: "Conditional Access"
|
||||||
graph_resource: "identity/conditionalAccess/policies"
|
graph_resource: "identity/conditionalAccess/policies"
|
||||||
@ -35,6 +75,14 @@ ## Scope
|
|||||||
graph_resource: "deviceManagement/deviceManagementScripts"
|
graph_resource: "deviceManagement/deviceManagementScripts"
|
||||||
notes: "scriptContent wird beim Backup base64-decoded gespeichert und beim Restore wieder encoded (vgl. FR-020)."
|
notes: "scriptContent wird beim Backup base64-decoded gespeichert und beim Restore wieder encoded (vgl. FR-020)."
|
||||||
|
|
||||||
|
- key: deviceShellScript
|
||||||
|
name: "macOS Shell Scripts"
|
||||||
|
graph_resource: "deviceManagement/deviceShellScripts"
|
||||||
|
|
||||||
|
- key: deviceHealthScript
|
||||||
|
name: "Proactive Remediations"
|
||||||
|
graph_resource: "deviceManagement/deviceHealthScripts"
|
||||||
|
|
||||||
- key: enrollmentRestriction
|
- key: enrollmentRestriction
|
||||||
name: "Enrollment Restrictions"
|
name: "Enrollment Restrictions"
|
||||||
graph_resource: "deviceManagement/deviceEnrollmentConfigurations"
|
graph_resource: "deviceManagement/deviceEnrollmentConfigurations"
|
||||||
@ -46,22 +94,40 @@ ## Scope
|
|||||||
- key: windowsEnrollmentStatusPage
|
- key: windowsEnrollmentStatusPage
|
||||||
name: "Enrollment Status Page (ESP)"
|
name: "Enrollment Status Page (ESP)"
|
||||||
graph_resource: "deviceManagement/deviceEnrollmentConfigurations"
|
graph_resource: "deviceManagement/deviceEnrollmentConfigurations"
|
||||||
filter: "odata.type eq '#microsoft.graph.windows10EnrollmentCompletionPageConfiguration'"
|
notes: "Filtered to #microsoft.graph.windows10EnrollmentCompletionPageConfiguration."
|
||||||
|
|
||||||
- key: endpointSecurityIntent
|
- key: endpointSecurityIntent
|
||||||
name: "Endpoint Security Intents"
|
name: "Endpoint Security Intents"
|
||||||
graph_resource: "deviceManagement/intents"
|
graph_resource: "deviceManagement/intents"
|
||||||
notes: "Account Protection, Disk Encryption etc.; Zuordnung über bekannte Templates."
|
notes: "Account Protection, Disk Encryption etc.; Zuordnung über bekannte Templates."
|
||||||
|
|
||||||
|
- key: endpointSecurityPolicy
|
||||||
|
name: "Endpoint Security Policies"
|
||||||
|
graph_resource: "deviceManagement/configurationPolicies"
|
||||||
|
notes: "Configuration policies classified via technologies/templateReference; restore execution enabled with template validation (Feature 023)."
|
||||||
|
|
||||||
|
- key: securityBaselinePolicy
|
||||||
|
name: "Security Baselines"
|
||||||
|
graph_resource: "deviceManagement/configurationPolicies"
|
||||||
|
notes: "High risk; v1 restore stays preview-only."
|
||||||
|
|
||||||
- key: mobileApp
|
- key: mobileApp
|
||||||
name: "Applications (Metadata only)"
|
name: "Applications (Metadata only)"
|
||||||
graph_resource: "deviceAppManagement/mobileApps"
|
graph_resource: "deviceAppManagement/mobileApps"
|
||||||
notes: "Backup nur von Metadaten/Zuweisungen (kein Binary-Download in v1)."
|
notes: "Backup nur von Metadaten/Zuweisungen (kein Binary-Download in v1)."
|
||||||
|
|
||||||
- key: settingsCatalogPolicy
|
foundation_types:
|
||||||
name: "Settings Catalog Policy"
|
- key: assignmentFilter
|
||||||
graph_resource: "deviceManagement/configurationPolicies"
|
name: "Assignment Filter"
|
||||||
notes: "Intune Settings Catalog Policies liegen NICHT unter deviceConfigurations, sondern unter configurationPolicies. v1 behandelt sie als eigenen Typ."
|
graph_resource: "deviceManagement/assignmentFilters"
|
||||||
|
|
||||||
|
- key: roleScopeTag
|
||||||
|
name: "Scope Tag"
|
||||||
|
graph_resource: "deviceManagement/roleScopeTags"
|
||||||
|
|
||||||
|
- key: notificationMessageTemplate
|
||||||
|
name: "Notification Message Template"
|
||||||
|
graph_resource: "deviceManagement/notificationMessageTemplates"
|
||||||
|
|
||||||
restore_matrix:
|
restore_matrix:
|
||||||
deviceConfiguration:
|
deviceConfiguration:
|
||||||
@ -70,6 +136,37 @@ ## Scope
|
|||||||
risk: medium
|
risk: medium
|
||||||
notes: "Standard-Case für Backup+Restore; starke Preview/Audit Pflicht."
|
notes: "Standard-Case für Backup+Restore; starke Preview/Audit Pflicht."
|
||||||
|
|
||||||
|
groupPolicyConfiguration:
|
||||||
|
backup: full
|
||||||
|
restore: enabled
|
||||||
|
risk: medium
|
||||||
|
|
||||||
|
settingsCatalogPolicy:
|
||||||
|
backup: full
|
||||||
|
restore: enabled
|
||||||
|
risk: medium
|
||||||
|
notes: "Settings are applied via configurationPolicies/{id}/settings; capability fallbacks may require manual follow-up."
|
||||||
|
|
||||||
|
windowsUpdateRing:
|
||||||
|
backup: full
|
||||||
|
restore: enabled
|
||||||
|
risk: medium-high
|
||||||
|
|
||||||
|
windowsFeatureUpdateProfile:
|
||||||
|
backup: full
|
||||||
|
restore: enabled
|
||||||
|
risk: high
|
||||||
|
|
||||||
|
windowsQualityUpdateProfile:
|
||||||
|
backup: full
|
||||||
|
restore: enabled
|
||||||
|
risk: high
|
||||||
|
|
||||||
|
windowsDriverUpdateProfile:
|
||||||
|
backup: full
|
||||||
|
restore: enabled
|
||||||
|
risk: high
|
||||||
|
|
||||||
deviceCompliancePolicy:
|
deviceCompliancePolicy:
|
||||||
backup: full
|
backup: full
|
||||||
restore: enabled
|
restore: enabled
|
||||||
@ -82,6 +179,16 @@ ## Scope
|
|||||||
risk: medium-high
|
risk: medium-high
|
||||||
notes: "MAM-Änderungen wirken auf Datenzugriff in Apps; Preview und Diff wichtig."
|
notes: "MAM-Änderungen wirken auf Datenzugriff in Apps; Preview und Diff wichtig."
|
||||||
|
|
||||||
|
mamAppConfiguration:
|
||||||
|
backup: full
|
||||||
|
restore: enabled
|
||||||
|
risk: medium-high
|
||||||
|
|
||||||
|
managedDeviceAppConfiguration:
|
||||||
|
backup: full
|
||||||
|
restore: enabled
|
||||||
|
risk: medium-high
|
||||||
|
|
||||||
conditionalAccessPolicy:
|
conditionalAccessPolicy:
|
||||||
backup: full
|
backup: full
|
||||||
restore: preview-only
|
restore: preview-only
|
||||||
@ -94,6 +201,16 @@ ## Scope
|
|||||||
risk: medium
|
risk: medium
|
||||||
notes: "Script-Inhalt und Einstellungen werden gesichert; Decode/Encode beachten."
|
notes: "Script-Inhalt und Einstellungen werden gesichert; Decode/Encode beachten."
|
||||||
|
|
||||||
|
deviceShellScript:
|
||||||
|
backup: full
|
||||||
|
restore: enabled
|
||||||
|
risk: medium
|
||||||
|
|
||||||
|
deviceHealthScript:
|
||||||
|
backup: full
|
||||||
|
restore: enabled
|
||||||
|
risk: medium
|
||||||
|
|
||||||
enrollmentRestriction:
|
enrollmentRestriction:
|
||||||
backup: full
|
backup: full
|
||||||
restore: preview-only
|
restore: preview-only
|
||||||
@ -118,17 +235,38 @@ ## Scope
|
|||||||
risk: high
|
risk: high
|
||||||
notes: "Security-relevante Einstellungen (z. B. Credential Guard); Preview + klare Konflikt-Warnungen nötig."
|
notes: "Security-relevante Einstellungen (z. B. Credential Guard); Preview + klare Konflikt-Warnungen nötig."
|
||||||
|
|
||||||
settingsCatalogPolicy:
|
endpointSecurityPolicy:
|
||||||
backup: full
|
backup: full
|
||||||
restore: enableds
|
restore: enabled
|
||||||
risk: medium
|
risk: high
|
||||||
notes: "Settings Catalog Policies sind Standard-Config-Policies (Settings Catalog). Preview/Audit Pflicht; Restore automatisierbar."
|
notes: "Enabled with template validation (Feature 023)."
|
||||||
|
|
||||||
|
securityBaselinePolicy:
|
||||||
|
backup: full
|
||||||
|
restore: preview-only
|
||||||
|
risk: high
|
||||||
|
notes: "High risk; preview-only by default."
|
||||||
|
|
||||||
mobileApp:
|
mobileApp:
|
||||||
backup: metadata-only
|
backup: metadata-only
|
||||||
restore: enabled
|
restore: enabled
|
||||||
risk: low-medium
|
risk: low-medium
|
||||||
notes: "Nur Metadaten/Zuweisungen; kein Binary; Restore setzt Konfigurationen/Zuweisungen wieder."
|
notes: "Nur Metadaten/Zuweisungen; kein Binary; Restore setzt Konfigurationen/Zuweisungen wieder."
|
||||||
|
|
||||||
|
assignmentFilter:
|
||||||
|
backup: full
|
||||||
|
restore: enabled
|
||||||
|
risk: low
|
||||||
|
|
||||||
|
roleScopeTag:
|
||||||
|
backup: full
|
||||||
|
restore: enabled
|
||||||
|
risk: low
|
||||||
|
|
||||||
|
notificationMessageTemplate:
|
||||||
|
backup: full
|
||||||
|
restore: enabled
|
||||||
|
risk: low
|
||||||
```
|
```
|
||||||
|
|
||||||
## User Scenarios & Testing *(mandatory)*
|
## User Scenarios & Testing *(mandatory)*
|
||||||
|
|||||||
@ -8,9 +8,9 @@ # Tasks: TenantPilot v1
|
|||||||
**Prerequisites**: plan.md (complete), spec.md (complete)
|
**Prerequisites**: plan.md (complete), spec.md (complete)
|
||||||
|
|
||||||
**Status snapshot**
|
**Status snapshot**
|
||||||
- Done: Phases 1–13 (US1–US4, Settings normalization/display, Highlander, US6 permissions/health, housekeeping/UX, ops)
|
- Done: Phases 1–15 (US1–US8, Settings Catalog hydration/display, restore rerun, Highlander, US6 permissions/health, housekeeping/UX, ops)
|
||||||
- Next up: Phase 14 (US7) delegated Intune RBAC onboarding wizard (synchronous)
|
- Open: T167 (optional) CLI/Job for CHECK/REPORT only (no grant)
|
||||||
- Upcoming: Phase 15 (US8) Graph Contract Registry & Drift Guard
|
- Next up: Feature 018 (Driver Updates / WUfB add-on) in `specs/018-driver-updates-wufb/`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -188,7 +188,7 @@ ## Acceptance Criteria
|
|||||||
- Restore von `settingsCatalogPolicy` scheitert nicht mehr an `Platforms`.
|
- Restore von `settingsCatalogPolicy` scheitert nicht mehr an `Platforms`.
|
||||||
- Results zeigen bei Fehlern weiterhin request-id/client-request-id (bleibt wie T177).
|
- Results zeigen bei Fehlern weiterhin request-id/client-request-id (bleibt wie T177).
|
||||||
|
|
||||||
- [ ] T179 [US1b][Scope][settingsCatalogPolicy] Hydrate Settings Catalog “Configuration settings” for snapshots + normalized display
|
- [x] T179 [US1b][Scope][settingsCatalogPolicy] Hydrate Settings Catalog “Configuration settings” for snapshots + normalized display
|
||||||
|
|
||||||
- **Goal:** Für `settingsCatalogPolicy` sollen die **Configuration settings** (wie im Intune Portal unter *Configuration settings*) im System sichtbar sein:
|
- **Goal:** Für `settingsCatalogPolicy` sollen die **Configuration settings** (wie im Intune Portal unter *Configuration settings*) im System sichtbar sein:
|
||||||
- in **Policy Version Raw JSON** enthalten
|
- in **Policy Version Raw JSON** enthalten
|
||||||
@ -278,7 +278,7 @@ ## Verification
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
- [ ] T180 [US1b][Bug][settingsCatalogPolicy] Hydrate Settings Catalog settings in Version capture + Policy detail uses hydrated snapshot
|
- [x] T180 [US1b][Bug][settingsCatalogPolicy] Hydrate Settings Catalog settings in Version capture + Policy detail uses hydrated snapshot
|
||||||
|
|
||||||
- **Goal:** `settingsCatalogPolicy` soll die *Configuration settings* nicht nur in Backups, sondern auch in **Policy Versions** enthalten, damit **Policy Detail**, Diff/Preview/Restore auf den echten Settings basieren.
|
- **Goal:** `settingsCatalogPolicy` soll die *Configuration settings* nicht nur in Backups, sondern auch in **Policy Versions** enthalten, damit **Policy Detail**, Diff/Preview/Restore auf den echten Settings basieren.
|
||||||
- **Why:** Aktuell hydriert nur `BackupService`, aber Policy Detail/Versions zeigen weiterhin nur Base-Metadaten.
|
- **Why:** Aktuell hydriert nur `BackupService`, aber Policy Detail/Versions zeigen weiterhin nur Base-Metadaten.
|
||||||
@ -610,7 +610,7 @@ ## Acceptance Criteria
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
- [ ]T185 [UX][US1b][settingsCatalogPolicy] Make Settings Catalog settings readable (label/value parsing + table ergonomics)
|
- [x] T185 [UX][US1b][settingsCatalogPolicy] Make Settings Catalog settings readable (label/value parsing + table ergonomics)
|
||||||
|
|
||||||
- **Goal:** Settings Catalog Policies sollen im Policy/Version Detail **für Admins lesbar** sein, ohne dass wir “alle Settings kennen müssen”.
|
- **Goal:** Settings Catalog Policies sollen im Policy/Version Detail **für Admins lesbar** sein, ohne dass wir “alle Settings kennen müssen”.
|
||||||
- Tabelle zeigt **sprechende Bezeichnung** + **kompakte Werte**
|
- Tabelle zeigt **sprechende Bezeichnung** + **kompakte Werte**
|
||||||
@ -699,7 +699,7 @@ ## Acceptance Criteria
|
|||||||
- **Readable Setting name** (not a cut-off vendor string)
|
- **Readable Setting name** (not a cut-off vendor string)
|
||||||
- **Readable Value preview** (True/False/12/etc.)
|
- **Readable Value preview** (True/False/12/etc.)
|
||||||
|
|
||||||
- [ ] T186 [US4][Bugfix][settingsCatalogPolicy] Fix settings_apply payload typing (@odata.type) + body shape for configurationPolicies/{id}/settings
|
- [x] T186 [US4][Bugfix][settingsCatalogPolicy] Fix settings_apply payload typing (@odata.type) + body shape for configurationPolicies/{id}/settings
|
||||||
|
|
||||||
**Goal:** Restore für `settingsCatalogPolicy` soll Settings zuverlässig anwenden können, ohne ModelValidationFailure wegen fehlender/entfernter `@odata.type`.
|
**Goal:** Restore für `settingsCatalogPolicy` soll Settings zuverlässig anwenden können, ohne ModelValidationFailure wegen fehlender/entfernter `@odata.type`.
|
||||||
|
|
||||||
@ -787,7 +787,7 @@ ### Implementation for User Story 4
|
|||||||
- [x] T023 [US4] Implement restore service with preview/dry-run and selective item application in `app/Services/Intune/RestoreService.php`, integrating Graph adapter and conflict detection.
|
- [x] T023 [US4] Implement restore service with preview/dry-run and selective item application in `app/Services/Intune/RestoreService.php`, integrating Graph adapter and conflict detection.
|
||||||
- [x] T024 [US4] Add Filament restore UI (wizard or pages) showing preview, warnings, and confirmation gate in `app/Filament/Resources/RestoreRunResource.php`.
|
- [x] T024 [US4] Add Filament restore UI (wizard or pages) showing preview, warnings, and confirmation gate in `app/Filament/Resources/RestoreRunResource.php`.
|
||||||
- [x] T025 [US4] Record restore run lifecycle (start, per-item result, completion) and audit events in `restore_runs` and `audit_logs`.
|
- [x] T025 [US4] Record restore run lifecycle (start, per-item result, completion) and audit events in `restore_runs` and `audit_logs`.
|
||||||
- [ ] T156 [US4][UX] Add “Rerun” action to RestoreRun row actions (ActionGroup): creates a new RestoreRun cloned from selected run (same backup_set_id, same selected items, same dry_run flag), enforces same safety gates/confirmations as original execution path, writes audit event restore_run.rerun_created with source_restore_run_id.
|
- [x] T156 [US4][UX] Add “Rerun” action to RestoreRun row actions (ActionGroup): creates a new RestoreRun cloned from selected run (same backup_set_id, same selected items, same dry_run flag), enforces same safety gates/confirmations as original execution path, writes audit event restore_run.rerun_created with source_restore_run_id.
|
||||||
|
|
||||||
|
|
||||||
## Phase 7: User Story 5 - Operational readiness and environments (Priority: P2)
|
## Phase 7: User Story 5 - Operational readiness and environments (Priority: P2)
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
use App\Services\Intune\ManagedDeviceAppConfigurationNormalizer;
|
use App\Services\Intune\ManagedDeviceAppConfigurationNormalizer;
|
||||||
use App\Services\Intune\ScriptsPolicyNormalizer;
|
use App\Services\Intune\ScriptsPolicyNormalizer;
|
||||||
use App\Services\Intune\SettingsCatalogPolicyNormalizer;
|
use App\Services\Intune\SettingsCatalogPolicyNormalizer;
|
||||||
|
use App\Services\Intune\WindowsDriverUpdateProfileNormalizer;
|
||||||
use App\Services\Intune\WindowsFeatureUpdateProfileNormalizer;
|
use App\Services\Intune\WindowsFeatureUpdateProfileNormalizer;
|
||||||
use App\Services\Intune\WindowsQualityUpdateProfileNormalizer;
|
use App\Services\Intune\WindowsQualityUpdateProfileNormalizer;
|
||||||
use App\Services\Intune\WindowsUpdateRingNormalizer;
|
use App\Services\Intune\WindowsUpdateRingNormalizer;
|
||||||
@ -49,6 +50,7 @@ public function register(): void
|
|||||||
ManagedDeviceAppConfigurationNormalizer::class,
|
ManagedDeviceAppConfigurationNormalizer::class,
|
||||||
ScriptsPolicyNormalizer::class,
|
ScriptsPolicyNormalizer::class,
|
||||||
SettingsCatalogPolicyNormalizer::class,
|
SettingsCatalogPolicyNormalizer::class,
|
||||||
|
WindowsDriverUpdateProfileNormalizer::class,
|
||||||
WindowsFeatureUpdateProfileNormalizer::class,
|
WindowsFeatureUpdateProfileNormalizer::class,
|
||||||
WindowsQualityUpdateProfileNormalizer::class,
|
WindowsQualityUpdateProfileNormalizer::class,
|
||||||
WindowsUpdateRingNormalizer::class,
|
WindowsUpdateRingNormalizer::class,
|
||||||
|
|||||||
125
app/Services/Intune/WindowsDriverUpdateProfileNormalizer.php
Normal file
125
app/Services/Intune/WindowsDriverUpdateProfileNormalizer.php
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Intune;
|
||||||
|
|
||||||
|
use Carbon\CarbonImmutable;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
|
class WindowsDriverUpdateProfileNormalizer implements PolicyTypeNormalizer
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly DefaultPolicyNormalizer $defaultNormalizer,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function supports(string $policyType): bool
|
||||||
|
{
|
||||||
|
return $policyType === 'windowsDriverUpdateProfile';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{status: string, settings: array<int, array<string, mixed>>, settings_table?: array<string, mixed>, warnings: array<int, string>}
|
||||||
|
*/
|
||||||
|
public function normalize(?array $snapshot, string $policyType, ?string $platform = null): array
|
||||||
|
{
|
||||||
|
$snapshot = $snapshot ?? [];
|
||||||
|
$normalized = $this->defaultNormalizer->normalize($snapshot, $policyType, $platform);
|
||||||
|
|
||||||
|
if ($snapshot === []) {
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
$block = $this->buildDriverUpdateBlock($snapshot);
|
||||||
|
|
||||||
|
if ($block !== null) {
|
||||||
|
$normalized['settings'][] = $block;
|
||||||
|
$normalized['settings'] = array_values(array_filter($normalized['settings']));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function flattenForDiff(?array $snapshot, string $policyType, ?string $platform = null): array
|
||||||
|
{
|
||||||
|
$snapshot = $snapshot ?? [];
|
||||||
|
$normalized = $this->normalize($snapshot, $policyType, $platform);
|
||||||
|
|
||||||
|
return $this->defaultNormalizer->flattenNormalizedForDiff($normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildDriverUpdateBlock(array $snapshot): ?array
|
||||||
|
{
|
||||||
|
$entries = [];
|
||||||
|
|
||||||
|
$displayName = Arr::get($snapshot, 'displayName');
|
||||||
|
|
||||||
|
if (is_string($displayName) && $displayName !== '') {
|
||||||
|
$entries[] = ['key' => 'Name', 'value' => $displayName];
|
||||||
|
}
|
||||||
|
|
||||||
|
$approvalType = Arr::get($snapshot, 'approvalType');
|
||||||
|
|
||||||
|
if (is_string($approvalType) && $approvalType !== '') {
|
||||||
|
$entries[] = ['key' => 'Approval type', 'value' => $approvalType];
|
||||||
|
}
|
||||||
|
|
||||||
|
$deferral = Arr::get($snapshot, 'deploymentDeferralInDays');
|
||||||
|
|
||||||
|
if (is_int($deferral) || (is_numeric($deferral) && (string) (int) $deferral === (string) $deferral)) {
|
||||||
|
$entries[] = ['key' => 'Deployment deferral (days)', 'value' => (int) $deferral];
|
||||||
|
}
|
||||||
|
|
||||||
|
$deviceReporting = Arr::get($snapshot, 'deviceReporting');
|
||||||
|
|
||||||
|
if (is_int($deviceReporting) || (is_numeric($deviceReporting) && (string) (int) $deviceReporting === (string) $deviceReporting)) {
|
||||||
|
$entries[] = ['key' => 'Devices reporting', 'value' => (int) $deviceReporting];
|
||||||
|
}
|
||||||
|
|
||||||
|
$newUpdates = Arr::get($snapshot, 'newUpdates');
|
||||||
|
|
||||||
|
if (is_int($newUpdates) || (is_numeric($newUpdates) && (string) (int) $newUpdates === (string) $newUpdates)) {
|
||||||
|
$entries[] = ['key' => 'New driver updates', 'value' => (int) $newUpdates];
|
||||||
|
}
|
||||||
|
|
||||||
|
$inventorySyncStatus = Arr::get($snapshot, 'inventorySyncStatus');
|
||||||
|
|
||||||
|
if (is_array($inventorySyncStatus)) {
|
||||||
|
$state = Arr::get($inventorySyncStatus, 'driverInventorySyncState');
|
||||||
|
|
||||||
|
if (is_string($state) && $state !== '') {
|
||||||
|
$entries[] = ['key' => 'Inventory sync state', 'value' => $state];
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastSuccessful = $this->formatDateTime(Arr::get($inventorySyncStatus, 'lastSuccessfulSyncDateTime'));
|
||||||
|
|
||||||
|
if ($lastSuccessful !== null) {
|
||||||
|
$entries[] = ['key' => 'Last successful inventory sync', 'value' => $lastSuccessful];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($entries === []) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'type' => 'keyValue',
|
||||||
|
'title' => 'Driver Update Profile',
|
||||||
|
'entries' => $entries,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatDateTime(mixed $value): ?string
|
||||||
|
{
|
||||||
|
if (! is_string($value) || $value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return CarbonImmutable::parse($value)->toDateTimeString();
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -296,6 +296,42 @@
|
|||||||
'assignments_delete_path' => '/deviceManagement/windowsQualityUpdateProfiles/{id}/assignments/{assignmentId}',
|
'assignments_delete_path' => '/deviceManagement/windowsQualityUpdateProfiles/{id}/assignments/{assignmentId}',
|
||||||
'assignments_delete_method' => 'DELETE',
|
'assignments_delete_method' => 'DELETE',
|
||||||
],
|
],
|
||||||
|
'windowsDriverUpdateProfile' => [
|
||||||
|
'resource' => 'deviceManagement/windowsDriverUpdateProfiles',
|
||||||
|
'allowed_select' => [
|
||||||
|
'id',
|
||||||
|
'displayName',
|
||||||
|
'description',
|
||||||
|
'@odata.type',
|
||||||
|
'createdDateTime',
|
||||||
|
'lastModifiedDateTime',
|
||||||
|
'approvalType',
|
||||||
|
'deploymentDeferralInDays',
|
||||||
|
'roleScopeTagIds',
|
||||||
|
],
|
||||||
|
'allowed_expand' => [],
|
||||||
|
'type_family' => [
|
||||||
|
'#microsoft.graph.windowsDriverUpdateProfile',
|
||||||
|
],
|
||||||
|
'create_method' => 'POST',
|
||||||
|
'update_method' => 'PATCH',
|
||||||
|
'id_field' => 'id',
|
||||||
|
'hydration' => 'properties',
|
||||||
|
'update_strip_keys' => [
|
||||||
|
'deviceReporting',
|
||||||
|
'newUpdates',
|
||||||
|
'inventorySyncStatus',
|
||||||
|
],
|
||||||
|
'assignments_list_path' => '/deviceManagement/windowsDriverUpdateProfiles/{id}/assignments',
|
||||||
|
'assignments_create_path' => '/deviceManagement/windowsDriverUpdateProfiles/{id}/assign',
|
||||||
|
'assignments_create_method' => 'POST',
|
||||||
|
'assignments_update_path' => '/deviceManagement/windowsDriverUpdateProfiles/{id}/assignments/{assignmentId}',
|
||||||
|
'assignments_update_method' => 'PATCH',
|
||||||
|
'assignments_delete_path' => '/deviceManagement/windowsDriverUpdateProfiles/{id}/assignments/{assignmentId}',
|
||||||
|
'assignments_delete_method' => 'DELETE',
|
||||||
|
'supports_scope_tags' => true,
|
||||||
|
'scope_tag_field' => 'roleScopeTagIds',
|
||||||
|
],
|
||||||
'deviceCompliancePolicy' => [
|
'deviceCompliancePolicy' => [
|
||||||
'resource' => 'deviceManagement/deviceCompliancePolicies',
|
'resource' => 'deviceManagement/deviceCompliancePolicies',
|
||||||
'allowed_select' => ['id', 'displayName', 'description', '@odata.type', 'version', 'lastModifiedDateTime'],
|
'allowed_select' => ['id', 'displayName', 'description', '@odata.type', 'version', 'lastModifiedDateTime'],
|
||||||
|
|||||||
@ -64,6 +64,16 @@
|
|||||||
'restore' => 'enabled',
|
'restore' => 'enabled',
|
||||||
'risk' => 'high',
|
'risk' => 'high',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'type' => 'windowsDriverUpdateProfile',
|
||||||
|
'label' => 'Driver Updates (Windows)',
|
||||||
|
'category' => 'Update Management',
|
||||||
|
'platform' => 'windows',
|
||||||
|
'endpoint' => 'deviceManagement/windowsDriverUpdateProfiles',
|
||||||
|
'backup' => 'full',
|
||||||
|
'restore' => 'enabled',
|
||||||
|
'risk' => 'high',
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'type' => 'deviceCompliancePolicy',
|
'type' => 'deviceCompliancePolicy',
|
||||||
'label' => 'Device Compliance',
|
'label' => 'Device Compliance',
|
||||||
|
|||||||
14
specs/018-driver-updates-wufb/checklists/requirements.md
Normal file
14
specs/018-driver-updates-wufb/checklists/requirements.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# Requirements Checklist (018)
|
||||||
|
|
||||||
|
**Created**: 2026-01-03
|
||||||
|
**Feature**: [spec.md](../spec.md)
|
||||||
|
|
||||||
|
- [x] `windowsDriverUpdateProfile` is added to `config/tenantpilot.php` (metadata, endpoint, backup/restore mode, risk).
|
||||||
|
- [x] Graph contract exists in `config/graph_contracts.php` (resource, type family, create/update methods, assignments paths).
|
||||||
|
- [x] Sync lists and stores driver update profiles in the Policies inventory.
|
||||||
|
- [x] Snapshot capture stores a complete payload for backups and versions.
|
||||||
|
- [x] Restore preview is available and respects the configured restore mode.
|
||||||
|
- [x] Restore execution applies only patchable properties and records audit logs.
|
||||||
|
- [x] Normalized settings view is readable for admins (no raw-only UX).
|
||||||
|
- [x] Pest tests cover sync + snapshot + restore + normalized display.
|
||||||
|
- [x] Pint run (`./vendor/bin/pint --dirty`) on touched files.
|
||||||
24
specs/018-driver-updates-wufb/plan.md
Normal file
24
specs/018-driver-updates-wufb/plan.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Plan: Driver Updates (WUfB Add-on) (018)
|
||||||
|
|
||||||
|
**Branch**: `feat/018-driver-updates-wufb`
|
||||||
|
**Date**: 2026-01-03
|
||||||
|
**Input**: [spec.md](./spec.md)
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Add first-class support for Windows Driver Update profiles (`windowsDriverUpdateProfile`) across inventory, backup/version snapshots, restore (preview + execution), and normalized display.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
1. Confirm Graph API details for driver update profiles (resource path, `@odata.type`, patchable properties, assignment endpoints).
|
||||||
|
2. Add type metadata to `config/tenantpilot.php` (category, endpoint, backup/restore mode, risk).
|
||||||
|
3. Add Graph contract entry in `config/graph_contracts.php` (resource, type family, create/update methods, assignments).
|
||||||
|
4. Ensure sync lists and stores these policies (config-driven loop) and add a targeted sync test.
|
||||||
|
5. Ensure snapshots capture the complete payload and add tests for version/backup capture.
|
||||||
|
6. Implement restore apply via contract-driven sanitization; add failure-safe behavior and tests.
|
||||||
|
7. Add a normalizer for readable UI output; add tests for normalized display.
|
||||||
|
8. Run Pint and targeted tests.
|
||||||
|
|
||||||
|
## Decisions / Notes
|
||||||
|
- Default to contract-driven restore semantics; avoid bespoke Graph calls unless strictly required.
|
||||||
|
- If Graph rejects PATCH due to read-only fields, extend `update_strip_keys` for this type (do not loosen safety).
|
||||||
|
- Keep restore risk high; require clear preview and audit trail.
|
||||||
|
|
||||||
79
specs/018-driver-updates-wufb/spec.md
Normal file
79
specs/018-driver-updates-wufb/spec.md
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
# Feature Specification: Driver Updates (WUfB Add-on) (018)
|
||||||
|
|
||||||
|
**Feature Branch**: `feat/018-driver-updates-wufb`
|
||||||
|
**Created**: 2026-01-03
|
||||||
|
**Status**: Implemented
|
||||||
|
**Priority**: P1
|
||||||
|
|
||||||
|
## Context
|
||||||
|
TenantPilot already covers core Windows Update for Business (WUfB) objects like:
|
||||||
|
- Update Rings (`windowsUpdateRing`)
|
||||||
|
- Feature Update Profiles (`windowsFeatureUpdateProfile`)
|
||||||
|
- Quality Update Profiles (`windowsQualityUpdateProfile`)
|
||||||
|
|
||||||
|
This feature adds **Windows Driver Updates** coverage to the same Update Management area so driver rollout configuration can be inventoried, snapshotted, diffed, and restored safely.
|
||||||
|
|
||||||
|
## In Scope
|
||||||
|
- New policy type: `windowsDriverUpdateProfile`
|
||||||
|
- Inventory/sync: list driver update profiles from Microsoft Graph and store them as policies.
|
||||||
|
- Snapshot capture: full snapshot of the profile payload (and assignments where supported).
|
||||||
|
- Restore:
|
||||||
|
- Preview/dry-run with diff + risk checks.
|
||||||
|
- Execution (PATCH/POST) as allowed by Graph, with audit logging.
|
||||||
|
- UI: normalized settings display (readable, admin-focused).
|
||||||
|
|
||||||
|
## Out of Scope (v1)
|
||||||
|
- Per-driver approval workflows / driver inventory insights.
|
||||||
|
- Advanced reporting on driver compliance.
|
||||||
|
- Partial per-setting restore.
|
||||||
|
|
||||||
|
## Graph API Details (confirmed)
|
||||||
|
- **Resource**: `deviceManagement/windowsDriverUpdateProfiles`
|
||||||
|
- **@odata.type**: `#microsoft.graph.windowsDriverUpdateProfile`
|
||||||
|
- **Patchable fields**: `displayName`, `description`, `approvalType`, `deploymentDeferralInDays`, `roleScopeTagIds`
|
||||||
|
- **Read-only fields (strip on PATCH)**: `deviceReporting`, `newUpdates`, `inventorySyncStatus`, `createdDateTime`, `lastModifiedDateTime`
|
||||||
|
- **Assignments**:
|
||||||
|
- list: `/deviceManagement/windowsDriverUpdateProfiles/{id}/assignments`
|
||||||
|
- assign action: `/deviceManagement/windowsDriverUpdateProfiles/{id}/assign`
|
||||||
|
- update/delete: `/deviceManagement/windowsDriverUpdateProfiles/{id}/assignments/{assignmentId}`
|
||||||
|
|
||||||
|
## User Scenarios & Testing
|
||||||
|
|
||||||
|
### User Story 1 — Inventory + readable view (P1)
|
||||||
|
As an admin, I can see Windows Driver Update profiles in the Policies list and view their configuration in a readable way.
|
||||||
|
|
||||||
|
**Acceptance**
|
||||||
|
1. Driver update profiles appear in the policy inventory with the correct type and category.
|
||||||
|
2. Policy detail shows a normalized settings table (not only raw JSON).
|
||||||
|
3. Policy Versions render “Normalized settings” consistently.
|
||||||
|
|
||||||
|
### User Story 2 — Snapshot capture (P1)
|
||||||
|
As an admin, when I capture a version or add a driver update profile to a backup set, the snapshot contains all relevant settings.
|
||||||
|
|
||||||
|
**Acceptance**
|
||||||
|
1. Snapshot stores the full Graph payload in JSON (immutable).
|
||||||
|
2. Any non-patchable/read-only properties are still preserved in the snapshot (but not sent on restore).
|
||||||
|
|
||||||
|
### User Story 3 — Restore preview + execution (P1)
|
||||||
|
As an admin, I can restore a driver update profile from a snapshot with a clear preview and safe execution.
|
||||||
|
|
||||||
|
**Acceptance**
|
||||||
|
1. Preview shows what would change and blocks if risk checks fail.
|
||||||
|
2. Execution applies only patchable properties (contract-driven sanitization).
|
||||||
|
3. Restore results include Graph error details (request-id, client-request-id, path/method) on failure.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
- **FR-001**: Add `windowsDriverUpdateProfile` to `config/tenantpilot.php` with category “Update Management”.
|
||||||
|
- **FR-002**: Add Graph contract entry for `windowsDriverUpdateProfile` in `config/graph_contracts.php` (resource, type family, create/update methods, assignments paths).
|
||||||
|
- **FR-003**: Ensure `PolicySyncService` syncs driver update profiles via config-driven type list.
|
||||||
|
- **FR-004**: Ensure `PolicySnapshotService` captures a complete payload for this type.
|
||||||
|
- **FR-005**: Ensure `RestoreService` applies snapshots using contract-driven sanitization and audit logging.
|
||||||
|
- **FR-006**: Add normalized display support for the key driver update profile fields.
|
||||||
|
- **FR-007**: Add automated Pest tests for sync + snapshot + restore preview/execution.
|
||||||
|
|
||||||
|
### Non-Functional Requirements
|
||||||
|
- **NFR-001**: Preserve tenant isolation and least privilege.
|
||||||
|
- **NFR-002**: Keep restore safe-by-default (preview/confirmation/audit).
|
||||||
|
- **NFR-003**: No new external services or dependencies.
|
||||||
32
specs/018-driver-updates-wufb/tasks.md
Normal file
32
specs/018-driver-updates-wufb/tasks.md
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# Tasks: Driver Updates (WUfB Add-on) (018)
|
||||||
|
|
||||||
|
**Branch**: `feat/018-driver-updates-wufb`
|
||||||
|
**Date**: 2026-01-03
|
||||||
|
**Input**: [spec.md](./spec.md), [plan.md](./plan.md)
|
||||||
|
|
||||||
|
## Phase 1: Setup
|
||||||
|
- [x] T001 Create/confirm spec, plan, tasks, checklist.
|
||||||
|
|
||||||
|
## Phase 2: Research & Design
|
||||||
|
- [x] T002 Verify Graph resource + `@odata.type` for driver update profiles.
|
||||||
|
- [x] T003 Verify PATCHable fields and define `update_strip_keys` / `update_whitelist`.
|
||||||
|
- [x] T004 Verify assignment endpoints (`/assignments`, `/assign`) for this resource.
|
||||||
|
- [x] T005 Decide restore mode (`enabled` vs `preview-only`) based on risk + patchability.
|
||||||
|
|
||||||
|
## Phase 3: Tests (TDD)
|
||||||
|
- [x] T006 Add sync test ensuring `windowsDriverUpdateProfile` policies are imported and typed correctly.
|
||||||
|
- [x] T007 Add snapshot/version capture test asserting full payload is stored.
|
||||||
|
- [x] T008 Add restore preview test for this type (entries + restore_mode shown).
|
||||||
|
- [x] T009 Add restore execution test asserting only patchable properties are sent and failures are reported with Graph metadata.
|
||||||
|
- [x] T010 Add normalized display test for key fields.
|
||||||
|
|
||||||
|
## Phase 4: Implementation
|
||||||
|
- [x] T011 Add `windowsDriverUpdateProfile` to `config/tenantpilot.php`.
|
||||||
|
- [x] T012 Add Graph contract entry in `config/graph_contracts.php`.
|
||||||
|
- [x] T013 Implement any required snapshot hydration (if Graph uses subresources).
|
||||||
|
- [x] T014 Implement restore apply support in `RestoreService` (contract-driven sanitization).
|
||||||
|
- [x] T015 Add a `WindowsDriverUpdateProfileNormalizer` and register it.
|
||||||
|
|
||||||
|
## Phase 5: Verification
|
||||||
|
- [x] T016 Run targeted tests.
|
||||||
|
- [x] T017 Run Pint (`./vendor/bin/pint --dirty`).
|
||||||
@ -3,12 +3,11 @@ # Requirements Checklist (023)
|
|||||||
**Created**: 2026-01-03
|
**Created**: 2026-01-03
|
||||||
**Feature**: [spec.md](../spec.md)
|
**Feature**: [spec.md](../spec.md)
|
||||||
|
|
||||||
- [ ] `endpointSecurityPolicy.restore` is changed to `enabled` in `config/tenantpilot.php`.
|
- [x] `endpointSecurityPolicy.restore` is changed to `enabled` in `config/tenantpilot.php`.
|
||||||
- [ ] Restore preview validates template existence and reports missing/ambiguous templates.
|
- [x] Restore preview validates template existence and reports missing/ambiguous templates.
|
||||||
- [ ] Restore execution blocks on missing/ambiguous templates with a clear, actionable error message.
|
- [x] Restore execution blocks on missing/ambiguous templates with a clear, actionable error message.
|
||||||
- [ ] Settings instances are validated against resolved template definitions before execution.
|
- [x] Settings instances are validated against resolved template definitions before execution.
|
||||||
- [ ] Template mapping strategy is defined for cross-tenant differences (if required) and is tested.
|
- [x] Template mapping strategy is defined for cross-tenant differences (if required) and is tested.
|
||||||
- [ ] Restore create + update paths for Endpoint Security policies are covered by automated tests.
|
- [x] Restore create + update paths for Endpoint Security policies are covered by automated tests.
|
||||||
- [ ] Assignments mapping/application for Endpoint Security policies are covered by automated tests.
|
- [x] Assignments mapping/application for Endpoint Security policies are covered by automated tests.
|
||||||
- [ ] Audit log entries exist for restore execution attempts (success and failure).
|
- [x] Audit log entries exist for restore execution attempts (success and failure).
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ # Plan: Endpoint Security Policy Restore (023)
|
|||||||
**Branch**: `feat/023-endpoint-security-restore`
|
**Branch**: `feat/023-endpoint-security-restore`
|
||||||
**Date**: 2026-01-03
|
**Date**: 2026-01-03
|
||||||
**Input**: [spec.md](./spec.md)
|
**Input**: [spec.md](./spec.md)
|
||||||
|
**Status**: Implemented (ready to merge)
|
||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
Enable full restore execution for Endpoint Security Policies (`endpointSecurityPolicy`) instead of preview-only, with defensive validation around templates and settings payloads.
|
Enable full restore execution for Endpoint Security Policies (`endpointSecurityPolicy`) instead of preview-only, with defensive validation around templates and settings payloads.
|
||||||
@ -29,4 +30,4 @@ ## Approach
|
|||||||
## Decisions / Notes
|
## Decisions / Notes
|
||||||
- Assume template identifiers may differ across tenants; prefer mapping by `templateFamily` with display-name fallback when required.
|
- Assume template identifiers may differ across tenants; prefer mapping by `templateFamily` with display-name fallback when required.
|
||||||
- Safety-first: if template resolution is ambiguous, treat as missing and block execution.
|
- Safety-first: if template resolution is ambiguous, treat as missing and block execution.
|
||||||
|
- Incident hardening: make restore failures actionable by surfacing Graph path/method and avoid unsafe fallback endpoints.
|
||||||
|
|||||||
@ -2,13 +2,13 @@ # Feature Specification: Enable Endpoint Security Policy Restore (023)
|
|||||||
|
|
||||||
**Feature Branch**: `feat/023-endpoint-security-restore`
|
**Feature Branch**: `feat/023-endpoint-security-restore`
|
||||||
**Created**: 2026-01-03
|
**Created**: 2026-01-03
|
||||||
**Status**: Draft
|
**Status**: Implemented (ready to merge)
|
||||||
**Priority**: P1 (Quick Win)
|
**Priority**: P1 (Quick Win)
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
Endpoint Security Policies are already in the `tenantpilot.php` config as `endpointSecurityPolicy` with `restore => 'preview-only'`. Based on Microsoft's recommendation to use the unified `deviceManagement/configurationPolicies` endpoint (over the deprecated `intents` API for new creations), we should enable full restore for this type.
|
Endpoint Security Policies are already in the `tenantpilot.php` config as `endpointSecurityPolicy` with `restore => 'preview-only'`. Based on Microsoft's recommendation to use the unified `deviceManagement/configurationPolicies` endpoint (over the deprecated `intents` API for new creations), we should enable full restore for this type.
|
||||||
|
|
||||||
This is a **configuration-only change** with additional validation/testing, not a new policy type implementation.
|
This is a **restore-mode enablement** with additional validation/testing and targeted restore hardening, not a new policy type implementation.
|
||||||
|
|
||||||
## User Scenarios & Testing
|
## User Scenarios & Testing
|
||||||
|
|
||||||
|
|||||||
@ -8,25 +8,30 @@ ## Phase 1: Setup
|
|||||||
- [x] T001 Create spec/plan/tasks and checklist.
|
- [x] T001 Create spec/plan/tasks and checklist.
|
||||||
|
|
||||||
## Phase 2: Inventory & Design
|
## Phase 2: Inventory & Design
|
||||||
- [ ] T002 Confirm current restore mode + code paths for `endpointSecurityPolicy` (`config/tenantpilot.php`, restore services).
|
- [x] T002 Confirm current restore mode + code paths for `endpointSecurityPolicy` (`config/tenantpilot.php`, restore services).
|
||||||
- [ ] T003 Decide template resolution strategy (ID vs family/display name) and required Graph calls.
|
- [x] T003 Decide template resolution strategy (ID vs family/display name) and required Graph calls.
|
||||||
- [ ] T004 Define settings instance validation rules (warning vs block) for restore preview/execution.
|
- [x] T004 Define settings instance validation rules (warning vs block) for restore preview/execution.
|
||||||
|
|
||||||
## Phase 3: Tests (TDD)
|
## Phase 3: Tests (TDD)
|
||||||
- [ ] T005 Add feature tests for restore execution create/update for `endpointSecurityPolicy`.
|
- [x] T005 Add feature tests for restore execution create/update for `endpointSecurityPolicy`.
|
||||||
- [ ] T006 Add feature tests for preview warnings when template is missing.
|
- [x] T006 Add feature tests for preview warnings when template is missing.
|
||||||
- [ ] T007 Add feature tests asserting restore execution fails gracefully when template is missing.
|
- [x] T007 Add feature tests asserting restore execution fails gracefully when template is missing.
|
||||||
- [ ] T008 Add tests for settings validation failure paths (invalid/unknown settings instances).
|
- [x] T008 Add tests for settings validation failure paths (invalid/unknown settings instances).
|
||||||
- [ ] T009 Add feature tests asserting assignments are applied for endpoint security policies.
|
- [x] T009 Add feature tests asserting assignments are applied for endpoint security policies.
|
||||||
|
|
||||||
## Phase 4: Implementation
|
## Phase 4: Implementation
|
||||||
- [ ] T010 Enable restore for `endpointSecurityPolicy` in `config/tenantpilot.php`.
|
- [x] T010 Enable restore for `endpointSecurityPolicy` in `config/tenantpilot.php`.
|
||||||
- [ ] T011 Implement template existence validation in restore preview and execution gating.
|
- [x] T011 Implement template existence validation in restore preview and execution gating.
|
||||||
- [ ] T012 Implement settings instance validation against resolved template definitions.
|
- [x] T012 Implement settings instance validation against resolved template definitions.
|
||||||
- [ ] T013 Implement template mapping (if required) and ensure restore payload uses mapped template reference.
|
- [x] T013 Implement template mapping (if required) and ensure restore payload uses mapped template reference.
|
||||||
- [ ] T014 Ensure restore applies assignments for endpoint security policies using existing mapping logic.
|
- [x] T014 Ensure restore applies assignments for endpoint security policies using existing mapping logic.
|
||||||
|
|
||||||
## Phase 5: Verification
|
## Phase 5: Verification
|
||||||
- [ ] T015 Run targeted tests.
|
- [x] T015 Run targeted tests.
|
||||||
- [ ] T016 Run Pint (`./vendor/bin/pint --dirty`).
|
- [x] T016 Run Pint (`./vendor/bin/pint --dirty`).
|
||||||
|
|
||||||
|
## Phase 6: Hardening (Incident-driven)
|
||||||
|
- [x] T017 Default unknown policy types to `preview-only` to avoid invalid Graph endpoints.
|
||||||
|
- [x] T018 Harden endpoint resolution fallback for configuration policy types (avoid `deviceManagement/{policyType}`).
|
||||||
|
- [x] T019 Surface Graph method/path in RestoreRun Results for faster debugging.
|
||||||
|
- [x] T020 Strip non-patchable fields for `endpointSecurityIntent` PATCH (`isAssigned`, `templateId`, `isMigratingToConfigurationPolicy`).
|
||||||
|
|||||||
@ -104,6 +104,77 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
expect($policyPreview['action'])->toBe('update');
|
expect($policyPreview['action'])->toBe('update');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('restore preview shows enabled restore mode for windows driver update profiles', function () {
|
||||||
|
app()->bind(GraphClientInterface::class, fn () => new class implements GraphClientInterface
|
||||||
|
{
|
||||||
|
public function listPolicies(string $policyType, array $options = []): GraphResponse
|
||||||
|
{
|
||||||
|
return new GraphResponse(true, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse
|
||||||
|
{
|
||||||
|
return new GraphResponse(true, ['payload' => []]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOrganization(array $options = []): GraphResponse
|
||||||
|
{
|
||||||
|
return new GraphResponse(true, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse
|
||||||
|
{
|
||||||
|
return new GraphResponse(true, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getServicePrincipalPermissions(array $options = []): GraphResponse
|
||||||
|
{
|
||||||
|
return new GraphResponse(true, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function request(string $method, string $path, array $options = []): GraphResponse
|
||||||
|
{
|
||||||
|
return new GraphResponse(true, []);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$tenant = Tenant::create([
|
||||||
|
'tenant_id' => 'tenant-driver-preview',
|
||||||
|
'name' => 'Tenant Preview',
|
||||||
|
'metadata' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$backupSet = BackupSet::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'name' => 'Backup',
|
||||||
|
'status' => 'completed',
|
||||||
|
'item_count' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$backupItem = BackupItem::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'backup_set_id' => $backupSet->id,
|
||||||
|
'policy_id' => null,
|
||||||
|
'policy_identifier' => 'wdp-1',
|
||||||
|
'policy_type' => 'windowsDriverUpdateProfile',
|
||||||
|
'platform' => 'windows',
|
||||||
|
'payload' => [
|
||||||
|
'@odata.type' => '#microsoft.graph.windowsDriverUpdateProfile',
|
||||||
|
'displayName' => 'Driver Updates A',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$service = app(RestoreService::class);
|
||||||
|
$preview = $service->preview($tenant, $backupSet, [$backupItem->id]);
|
||||||
|
|
||||||
|
expect($preview)->toHaveCount(1);
|
||||||
|
|
||||||
|
$policyPreview = $preview[0] ?? [];
|
||||||
|
expect($policyPreview['policy_type'] ?? null)->toBe('windowsDriverUpdateProfile');
|
||||||
|
expect($policyPreview['action'] ?? null)->toBe('create');
|
||||||
|
expect($policyPreview['restore_mode'] ?? null)->toBe('enabled');
|
||||||
|
});
|
||||||
|
|
||||||
test('restore preview warns about missing compliance notification templates', function () {
|
test('restore preview warns about missing compliance notification templates', function () {
|
||||||
app()->bind(GraphClientInterface::class, fn () => new class implements GraphClientInterface
|
app()->bind(GraphClientInterface::class, fn () => new class implements GraphClientInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@ -211,3 +211,88 @@ public function getServicePrincipalPermissions(array $options = []): GraphRespon
|
|||||||
expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('deployableContentDisplayName');
|
expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('deployableContentDisplayName');
|
||||||
expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('releaseDateDisplayName');
|
expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('releaseDateDisplayName');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('restore execution applies windows driver update profile with sanitized payload', function () {
|
||||||
|
$client = new WindowsUpdateProfilesRestoreGraphClient;
|
||||||
|
app()->instance(GraphClientInterface::class, $client);
|
||||||
|
|
||||||
|
$tenant = Tenant::create([
|
||||||
|
'tenant_id' => 'tenant-1',
|
||||||
|
'name' => 'Tenant One',
|
||||||
|
'metadata' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$policy = Policy::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'external_id' => 'policy-driver',
|
||||||
|
'policy_type' => 'windowsDriverUpdateProfile',
|
||||||
|
'display_name' => 'Driver Updates A',
|
||||||
|
'platform' => 'windows',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$backupSet = BackupSet::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'name' => 'Backup',
|
||||||
|
'status' => 'completed',
|
||||||
|
'item_count' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$backupPayload = [
|
||||||
|
'id' => 'policy-driver',
|
||||||
|
'@odata.type' => '#microsoft.graph.windowsDriverUpdateProfile',
|
||||||
|
'displayName' => 'Driver Updates A',
|
||||||
|
'description' => 'Drivers rollout policy',
|
||||||
|
'approvalType' => 'automatic',
|
||||||
|
'deploymentDeferralInDays' => 7,
|
||||||
|
'deviceReporting' => 12,
|
||||||
|
'newUpdates' => 3,
|
||||||
|
'inventorySyncStatus' => [
|
||||||
|
'driverInventorySyncState' => 'success',
|
||||||
|
'lastSuccessfulSyncDateTime' => '2026-01-01T00:00:00Z',
|
||||||
|
],
|
||||||
|
'roleScopeTagIds' => ['0'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$backupItem = BackupItem::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'backup_set_id' => $backupSet->id,
|
||||||
|
'policy_id' => $policy->id,
|
||||||
|
'policy_identifier' => $policy->external_id,
|
||||||
|
'policy_type' => $policy->policy_type,
|
||||||
|
'platform' => $policy->platform,
|
||||||
|
'payload' => $backupPayload,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = User::factory()->create(['email' => 'tester@example.com']);
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
$service = app(RestoreService::class);
|
||||||
|
$run = $service->execute(
|
||||||
|
tenant: $tenant,
|
||||||
|
backupSet: $backupSet,
|
||||||
|
selectedItemIds: [$backupItem->id],
|
||||||
|
dryRun: false,
|
||||||
|
actorEmail: $user->email,
|
||||||
|
actorName: $user->name,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($run->status)->toBe('completed');
|
||||||
|
expect($run->results[0]['status'])->toBe('applied');
|
||||||
|
|
||||||
|
expect(PolicyVersion::where('policy_id', $policy->id)->count())->toBe(1);
|
||||||
|
|
||||||
|
expect($client->applyPolicyCalls)->toHaveCount(1);
|
||||||
|
expect($client->applyPolicyCalls[0]['policyType'])->toBe('windowsDriverUpdateProfile');
|
||||||
|
expect($client->applyPolicyCalls[0]['policyId'])->toBe('policy-driver');
|
||||||
|
expect($client->applyPolicyCalls[0]['options']['method'] ?? null)->toBe('PATCH');
|
||||||
|
|
||||||
|
expect($client->applyPolicyCalls[0]['payload']['approvalType'])->toBe('automatic');
|
||||||
|
expect($client->applyPolicyCalls[0]['payload']['deploymentDeferralInDays'])->toBe(7);
|
||||||
|
expect($client->applyPolicyCalls[0]['payload']['roleScopeTagIds'])->toBe(['0']);
|
||||||
|
|
||||||
|
expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('id');
|
||||||
|
expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('@odata.type');
|
||||||
|
expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('deviceReporting');
|
||||||
|
expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('newUpdates');
|
||||||
|
expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('inventorySyncStatus');
|
||||||
|
});
|
||||||
|
|||||||
@ -62,7 +62,7 @@
|
|||||||
$supported = config('tenantpilot.supported_policy_types');
|
$supported = config('tenantpilot.supported_policy_types');
|
||||||
$byType = collect($supported)->keyBy('type');
|
$byType = collect($supported)->keyBy('type');
|
||||||
|
|
||||||
expect($byType)->toHaveKeys(['deviceConfiguration', 'windowsUpdateRing', 'windowsFeatureUpdateProfile', 'windowsQualityUpdateProfile']);
|
expect($byType)->toHaveKeys(['deviceConfiguration', 'windowsUpdateRing', 'windowsFeatureUpdateProfile', 'windowsQualityUpdateProfile', 'windowsDriverUpdateProfile']);
|
||||||
|
|
||||||
expect($byType['deviceConfiguration']['filter'] ?? null)
|
expect($byType['deviceConfiguration']['filter'] ?? null)
|
||||||
->toBe("not isof('microsoft.graph.windowsUpdateForBusinessConfiguration')");
|
->toBe("not isof('microsoft.graph.windowsUpdateForBusinessConfiguration')");
|
||||||
@ -75,6 +75,50 @@
|
|||||||
|
|
||||||
expect($byType['windowsQualityUpdateProfile']['endpoint'] ?? null)
|
expect($byType['windowsQualityUpdateProfile']['endpoint'] ?? null)
|
||||||
->toBe('deviceManagement/windowsQualityUpdateProfiles');
|
->toBe('deviceManagement/windowsQualityUpdateProfiles');
|
||||||
|
|
||||||
|
expect($byType['windowsDriverUpdateProfile']['endpoint'] ?? null)
|
||||||
|
->toBe('deviceManagement/windowsDriverUpdateProfiles');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('syncs windows driver update profiles from Graph', function () {
|
||||||
|
$tenant = Tenant::factory()->create([
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$logger = mock(GraphLogger::class);
|
||||||
|
|
||||||
|
$logger->shouldReceive('logRequest')
|
||||||
|
->zeroOrMoreTimes()
|
||||||
|
->andReturnNull();
|
||||||
|
|
||||||
|
$logger->shouldReceive('logResponse')
|
||||||
|
->zeroOrMoreTimes()
|
||||||
|
->andReturnNull();
|
||||||
|
|
||||||
|
mock(GraphClientInterface::class)
|
||||||
|
->shouldReceive('listPolicies')
|
||||||
|
->once()
|
||||||
|
->with('windowsDriverUpdateProfile', mockery::type('array'))
|
||||||
|
->andReturn(new GraphResponse(
|
||||||
|
success: true,
|
||||||
|
data: [
|
||||||
|
[
|
||||||
|
'id' => 'wdp-1',
|
||||||
|
'displayName' => 'Driver Updates A',
|
||||||
|
'@odata.type' => '#microsoft.graph.windowsDriverUpdateProfile',
|
||||||
|
'approvalType' => 'automatic',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
));
|
||||||
|
|
||||||
|
$service = app(PolicySyncService::class);
|
||||||
|
|
||||||
|
$service->syncPolicies($tenant, [
|
||||||
|
['type' => 'windowsDriverUpdateProfile', 'platform' => 'windows'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(Policy::query()->where('tenant_id', $tenant->id)->where('policy_type', 'windowsDriverUpdateProfile')->count())
|
||||||
|
->toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('includes managed device app configurations in supported types', function () {
|
it('includes managed device app configurations in supported types', function () {
|
||||||
|
|||||||
@ -41,6 +41,27 @@ public function getPolicy(string $policyType, string $policyId, array $options =
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($policyType === 'windowsDriverUpdateProfile') {
|
||||||
|
return new GraphResponse(success: true, data: [
|
||||||
|
'payload' => [
|
||||||
|
'id' => $policyId,
|
||||||
|
'displayName' => 'Driver Updates A',
|
||||||
|
'description' => 'Drivers rollout policy',
|
||||||
|
'@odata.type' => '#microsoft.graph.windowsDriverUpdateProfile',
|
||||||
|
'approvalType' => 'automatic',
|
||||||
|
'deploymentDeferralInDays' => 7,
|
||||||
|
'deviceReporting' => 12,
|
||||||
|
'newUpdates' => 3,
|
||||||
|
'roleScopeTagIds' => ['0'],
|
||||||
|
'inventorySyncStatus' => [
|
||||||
|
'@odata.type' => '#microsoft.graph.windowsDriverUpdateProfileInventorySyncStatus',
|
||||||
|
'driverInventorySyncState' => 'success',
|
||||||
|
'lastSuccessfulSyncDateTime' => '2026-01-01T00:00:00Z',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
return new GraphResponse(success: true, data: [
|
return new GraphResponse(success: true, data: [
|
||||||
'payload' => [
|
'payload' => [
|
||||||
'id' => $policyId,
|
'id' => $policyId,
|
||||||
@ -271,6 +292,41 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
expect($client->requests[0][3]['select'])->not->toContain('@odata.type');
|
expect($client->requests[0][3]['select'])->not->toContain('@odata.type');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('captures windows driver update profile snapshots with full payload', function () {
|
||||||
|
$client = new PolicySnapshotGraphClient;
|
||||||
|
app()->instance(GraphClientInterface::class, $client);
|
||||||
|
|
||||||
|
$tenant = Tenant::factory()->create([
|
||||||
|
'tenant_id' => 'tenant-driver',
|
||||||
|
'app_client_id' => 'client-123',
|
||||||
|
'app_client_secret' => 'secret-123',
|
||||||
|
'is_current' => true,
|
||||||
|
]);
|
||||||
|
$tenant->makeCurrent();
|
||||||
|
|
||||||
|
$policy = Policy::factory()->create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'external_id' => 'wdp-123',
|
||||||
|
'policy_type' => 'windowsDriverUpdateProfile',
|
||||||
|
'display_name' => 'Driver Updates A',
|
||||||
|
'platform' => 'windows',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$service = app(PolicySnapshotService::class);
|
||||||
|
$result = $service->fetch($tenant, $policy);
|
||||||
|
|
||||||
|
expect($result)->toHaveKey('payload');
|
||||||
|
expect($result['payload']['approvalType'] ?? null)->toBe('automatic');
|
||||||
|
expect($result['payload']['deploymentDeferralInDays'] ?? null)->toBe(7);
|
||||||
|
expect($result['payload']['deviceReporting'] ?? null)->toBe(12);
|
||||||
|
expect($result['payload']['newUpdates'] ?? null)->toBe(3);
|
||||||
|
expect($result['payload']['inventorySyncStatus']['driverInventorySyncState'] ?? null)->toBe('success');
|
||||||
|
|
||||||
|
expect($client->requests[0][0])->toBe('getPolicy');
|
||||||
|
expect($client->requests[0][1])->toBe('windowsDriverUpdateProfile');
|
||||||
|
expect($client->requests[0][2])->toBe('wdp-123');
|
||||||
|
});
|
||||||
|
|
||||||
test('falls back to metadata-only snapshot when mamAppConfiguration returns 500', function () {
|
test('falls back to metadata-only snapshot when mamAppConfiguration returns 500', function () {
|
||||||
$client = Mockery::mock(\App\Services\Graph\GraphClientInterface::class);
|
$client = Mockery::mock(\App\Services\Graph\GraphClientInterface::class);
|
||||||
$client->shouldReceive('getPolicy')
|
$client->shouldReceive('getPolicy')
|
||||||
|
|||||||
38
tests/Unit/WindowsDriverUpdateProfileNormalizerTest.php
Normal file
38
tests/Unit/WindowsDriverUpdateProfileNormalizerTest.php
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Services\Intune\PolicyNormalizer;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
uses(TestCase::class);
|
||||||
|
|
||||||
|
it('normalizes windows driver update profiles into readable settings', function () {
|
||||||
|
$normalizer = app(PolicyNormalizer::class);
|
||||||
|
|
||||||
|
$snapshot = [
|
||||||
|
'@odata.type' => '#microsoft.graph.windowsDriverUpdateProfile',
|
||||||
|
'displayName' => 'Driver Updates A',
|
||||||
|
'description' => 'Drivers rollout policy',
|
||||||
|
'approvalType' => 'automatic',
|
||||||
|
'deploymentDeferralInDays' => 7,
|
||||||
|
'deviceReporting' => 12,
|
||||||
|
'newUpdates' => 3,
|
||||||
|
'inventorySyncStatus' => [
|
||||||
|
'driverInventorySyncState' => 'success',
|
||||||
|
'lastSuccessfulSyncDateTime' => '2026-01-01T00:00:00Z',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $normalizer->normalize($snapshot, 'windowsDriverUpdateProfile', 'windows');
|
||||||
|
|
||||||
|
expect($result['status'])->toBe('success');
|
||||||
|
expect($result['settings'])->toBeArray()->not->toBeEmpty();
|
||||||
|
|
||||||
|
$driverBlock = collect($result['settings'])
|
||||||
|
->first(fn (array $block) => ($block['title'] ?? null) === 'Driver Update Profile');
|
||||||
|
|
||||||
|
expect($driverBlock)->not->toBeNull();
|
||||||
|
|
||||||
|
$keys = collect($driverBlock['entries'] ?? [])->pluck('key')->all();
|
||||||
|
|
||||||
|
expect($keys)->toContain('Approval type', 'Deployment deferral (days)', 'Devices reporting', 'New driver updates');
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user