Run this checklist against a production-like deployment over HTTPS with a fresh backup available. Record the date, application version, PHP/MariaDB versions, tester and evidence for each item. Do not record passwords, API keys or credential values in the evidence.
## Pre-flight and deployment
- [ ] The virtual host/document root is `public/`; the repository root, `.env`, SQL files and `storage/` are not web-accessible.
- [ ]`.env` is present outside the public root, has restrictive permissions (for example `chmod 600 .env`), and contains production-only values.
- [ ]`php bin/healthcheck.php` passes with no secret values printed.
- [ ] HTTPS is enabled and HTTP redirects to HTTPS; the certificate and hostname are valid.
- [ ] A backup was taken before UAT and its location/time is recorded separately from this checklist.
## Administrator
- [ ] Sign in with the bootstrap Administrator account; invalid credentials are rejected.
- [ ] The dashboard loads and the Administrator can view clients, jobcards, SLA data, reports and audit events.
- [ ] Create, deactivate and reactivate a test Accounts user and a test Technician user.
- [ ] Assign roles/permissions; verify an unauthorized permission is not granted by merely hiding a navigation link.
- [ ] Assign a Technician to a test jobcard and verify the assignment is visible in the expected workflow.
- [ ] Review audit events for login and test administrative changes; confirm timestamps and actor are present.
- [ ] Verify sensitive credentials are masked by default, access is permission-controlled, and reveal/access is audited (where that module is enabled).
## Accounts
- [ ] Create and edit a client, including contact details and preferred contact method.
- [ ] Add a primary contact and verify duplicate primary contacts are rejected.
- [ ] Create a jobcard with client, priority and requested work; verify its reference number and initial status.
- [ ] Assign a Technician, update the jobcard through the supported statuses, and verify status history.
- [ ] Configure an SLA agreement and verify allocated/used/remaining hours and period boundaries.
- [ ] View reports and export a report if permitted; verify exported data contains only intended fields and no internal notes or credentials.
- [ ] Verify validation errors are understandable and do not discard unrelated entered fields.
## Technician
- [ ] Sign in as a Technician and verify only assigned jobcards are accessible.
- [ ] Verify a Technician cannot access another Technician's jobcard by changing an ID in the URL or form payload.
Record the request URL/route, authenticated role, test fixture IDs, expected response, observed response, and evidence for each case. Use separate Technician accounts and at least two clients/jobcards so an ID change cannot accidentally target the same tenant.
- [ ]**SEC-01 — Technician cross-client IDOR (read):** Technician A requests Technician B's jobcard URL and an attachment URL belonging to that jobcard. Both requests return the same not-found/denied behavior as an unknown ID, and no client name, jobcard details, attachment bytes or metadata are disclosed.
- [ ]**SEC-02 — Technician cross-client IDOR (write):** Technician A submits status, notes, assignment or time-entry payloads with Technician B's jobcard ID. CSRF-valid requests are still denied by authorization, and the target jobcard, assignments and time entries remain unchanged.
- [ ]**SEC-03 — Own-technician time isolation:** Create one assigned jobcard with time recorded by Technician A and Technician B. Technician A's report/UI/export contains only A's hours; it does not include B's hours or another client's totals. Repeat with a direct report URL and CSV export.
- [ ]**SEC-04 — Credential canonical storage:** Create a credential containing a unique canary secret. The database row has `secret_ciphertext` and no plaintext `secret` field/value; normal views show a mask; only an authorized reveal returns the secret once, with no-store headers and an audit event. A different client's credential ID cannot be revealed.
- [ ]**SEC-05 — Attachment safe metadata:** Attempt traversal names (`../x.pdf`), executable/double extensions (`invoice.php.jpg`), MIME mismatches, oversized files, and client-visible without explicit approval. Each is rejected before storage. A valid image/PDF is stored under a generated server name, served with its validated MIME and `X-Content-Type-Options: nosniff`, and remains inaccessible through a different jobcard/client ID.
- [ ]**SEC-06 — Healthcheck schema contract:** Run `php bin/healthcheck.php` with valid configuration and confirm every current schema table is probed, output contains statuses only, and no password, APP_KEY, database DSN, SQL exception, or secret value is printed. Remove/rename one required table in a disposable database and confirm a non-zero failure.
- [ ]**SEC-07 — CSRF route assumptions:** For login, logout, client create/edit/contact, jobcard create/update, time, assignment, attachment, credential and SLA POSTs, submit with a missing token and a wrong token. Every request is rejected before mutation (HTTP 419 or the documented equivalent); the valid-token control succeeds. GET requests do not mutate state.
- [ ]**SEC-08 — Migration coverage and restore:** Restore a pre-current-schema backup to an isolated database, run `database/upgrade.sql` once and a second time, and confirm feature tables/columns, permissions and the one-SLA-per-client constraint are present. Resolve/record duplicate SLA rows before the unique constraint step; rerun healthcheck and verify representative clients, jobcards, time entries, credentials and attachments.