# 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.
