# TenantPilot Security Guidelines Status: 2026-05-15 Reference model: OWASP ASVS 5.0.0, OWASP Top 10, NIST SSDF, Laravel 12, Filament 5. ## Security Target TenantPilot manages critical Intune configuration and restore workflows. Treat tenant data, backup payloads, provider credentials, policy snapshots, audit logs, and operation runs as sensitive enterprise data. ## Current Strengths - Workspace and tenant isolation are constitutional non-negotiables. - Many policies return `Response::denyAsNotFound()`. - `UiEnforcement` centralizes disabled/hidden UI affordance behavior. - `ProviderCredential` uses encrypted array casts and hides payloads. - Graph access is routed through `GraphClientInterface`. - Audit and operation-run models already provide traceability. ## Top Security Findings | Risk | Evidence | Priority | Control | |---|---|---:|---| | Vulnerable dependencies | `composer audit`, `pnpm audit` | P0 | Patch, audit gates, approved exceptions only. | | Inconsistent policy coverage | Some resource-backed models lack obvious policies | P1 | Resource-policy matrix and tests. | | Production session/debug defaults need gating | `.env.example` has `APP_DEBUG=true`, `SESSION_ENCRYPT=false` for local | P1 | Deployment checklist enforces production env. | | File upload future risk | Filament warns about file path tampering and filenames | P2 | Private disks, random names, MIME validation, path tamper prevention. | | Graph beta default | `config/graph.php` defaults to `beta` | P2 | Endpoint-level version registry and contract tests. | ## Release Security Checklist - `composer audit` clean or explicitly risk-accepted. - `corepack pnpm audit --audit-level moderate` clean or explicitly risk-accepted. - `APP_DEBUG=false` in staging/production. - `APP_KEY` present and not rotated casually. - Session cookies are secure, same-site, and domain-scoped for production. - Provider credentials remain encrypted and never logged. - No secrets in config, docs, tests, fixtures, screenshots, or audit metadata. - Every write operation has policy authorization, explicit confirmation, and audit log. - Backup and restore flows have dry-run/preview where applicable. - Queue payloads contain identifiers, not secrets or raw credential payloads. - Health endpoint and uptime monitor are active. ## Checklist for New Filament Resources - Policy exists for the model or a spec documents why no policy is needed. - `canViewAny`, `canCreate`, `canEdit`, `canDelete` call policies or capability resolver consistently. - Tenant-owned resources scope queries by workspace and managed environment. - Global search is disabled unless View/Edit pages are safe and scoped. - Tables eager-load relationships shown in columns. - Empty states do not leak tenant existence. - Mutating actions are confirmation-gated and tested. - Bulk actions intentionally choose `*Any` policy semantics or per-record authorization. ## Checklist for File Uploads - Store on a private disk by default. - Use random storage filenames. - Store original filenames in a separate column if needed. - Restrict `acceptedFileTypes()` and `maxSize()`. - Use Laravel file validation rules for server-side validation. - Use `preventFilePathTampering()` when the workflow does not intentionally allow choosing existing disk files. - Do not render uploaded HTML/SVG inline unless sanitized and explicitly approved. - Signed URLs must be short-lived and tenant-authorized. ## Checklist for Admin Actions - Action name describes the business effect. - UI state uses `UiEnforcement` or `WorkspaceUiEnforcement`. - Server handler calls `Gate::authorize()` or a policy method. - Destructive/high-impact action has `requiresConfirmation()`. - Handler writes an audit event with actor, workspace, managed environment, target, outcome, and safe metadata. - Long-running work dispatches a job and creates/updates an `OperationRun`. - Duplicate clicks are idempotent or guarded by locks/unique run identity. - Test covers allowed, disabled/denied, side effect, audit, and tenant isolation. ## Checklist for Multi-Tenancy - Workspace context is established before tenant context. - Non-members receive deny-as-not-found. - Queries filter by `workspace_id` and tenant id before access. - Cross-tenant surfaces are explicit and aggregation-based. - IDs from request/query strings are resolved through scoped resolvers. - Tests include tenant A cannot see or mutate tenant B. - Audit logs include workspace and tenant context when applicable. ## Security Code Pattern: Policy ```php namespace App\Policies; use App\Models\BackupSet; use App\Models\User; use Illuminate\Auth\Access\Response; final class BackupSetPolicy { public function view(User $user, BackupSet $backupSet): Response { if (! $backupSet->workspace || ! $user->belongsToWorkspace($backupSet->workspace)) { return Response::denyAsNotFound(); } return $user->can('tenant.view', $backupSet->managedEnvironment) ? Response::allow() : Response::denyAsNotFound(); } public function restore(User $user, BackupSet $backupSet): Response { if ($this->view($user, $backupSet)->denied()) { return Response::denyAsNotFound(); } return $user->can('tenant.restore.run', $backupSet->managedEnvironment) ? Response::allow() : Response::deny('Missing restore capability.'); } } ``` ## Security Code Pattern: Audit Event ```php $audit->record( action: 'backup_schedule.run_requested', actor: $actor, workspace: $schedule->workspace, managedEnvironment: $schedule->managedEnvironment, target: $schedule, metadata: [ 'operation_run_id' => $run->getKey(), 'schedule_id' => $schedule->getKey(), ], ); ``` Never include tokens, client secrets, raw credential payloads, or raw Graph error bodies in audit metadata.