# Ogun State Asset Management Production Audit

Reviewed repository:

- Application: `C:\Users\PLAYHOUSE\Herd\ogsg-asset-management`

## A. Executive Summary

**Overall production readiness score:** 4/10

**Launch readiness verdict:** Internally testable after test-suite repair. Not production-ready.

The application has a useful Laravel foundation for an Ogun State asset management platform. It includes Laravel web authentication, Spatie roles and permissions, UUID route keys, password hashing, login throttling, email verification routes, MDA/ministry records, asset categories, asset registration, asset requests, sequential approvals, procurement orders, supplier quote/delivery flows, PDF/Excel reporting, soft deletes, and some feature tests.

However, several production blockers remain. The most serious risks are broken MDA ownership checks, client-controlled ministry assignment during request creation, open public registration, inactive users not being blocked during login, weak durable audit logging, fragile asset import, incomplete upload validation, missing lifecycle modules, and a test suite that currently fails before running.

For a government/public-sector environment, the app is not yet ready to hold real asset records. It needs authorization hardening, audit accountability, lifecycle governance, data-integrity fixes, operational security controls, and broader regression coverage before launch.

### Biggest Risks

- Asset and asset request policies compare `ministry_id` to `user.id` instead of `user.ministry_id`.
- Ministry users submit `ministry_id` from the form when creating asset requests, creating a cross-MDA spoofing risk.
- Public registration is enabled, and newly registered users are logged in immediately.
- `is_active` exists on users but is not enforced during login.
- The test suite fails before execution because Pest test-case binding is duplicated.
- Activity logging writes to Laravel log channels while an `activity_logs` table exists but is not used by the service.
- Asset import maps fields that do not match the actual asset schema/model.
- Asset media deletion accepts submitted media IDs without sufficiently obvious ownership hardening.
- Asset upload handling is present in the controller but missing corresponding `files.*` validation.
- Transfer, maintenance, disposal, and physical verification are not implemented as first-class public-sector workflows.

### Best Parts of the App

- Authenticated app routes are grouped behind `auth` and `verified` middleware.
- Login throttling is implemented with Laravel's rate limiter.
- Session regeneration on login and invalidation on logout are implemented.
- Password rules require minimum length, letters, mixed case, numbers, symbols, and uncompromised passwords.
- Spatie roles and permissions are used for high-level RBAC.
- UUID route keys reduce simple sequential ID guessing.
- Asset tags have a unique database constraint.
- Soft deletes are added to core tables.
- Request approvals support sequential approval levels.
- Procurement order, quote, confirmation, cancellation, delivery, and asset creation flows exist.
- PDF and Excel exports exist for reports/procurement.
- Asset list pagination exists on the admin asset index.
- Some authorization and workflow feature tests exist, even though the suite currently needs repair.

### Critical Blockers Before Launch

- Fix MDA ownership checks in `AssetPolicy` and `AssetRequestPolicy`.
- Stop accepting `ministry_id` from ministry request forms; derive it from the authenticated user's MDA.
- Disable public registration in production or replace it with invite/admin provisioning.
- Enforce `is_active` during login.
- Repair the Pest test configuration so `php artisan test` runs reliably.
- Replace log-file-only activity logging with durable database-backed audit events.
- Fix asset import field mapping and validate imported MDA/category/tag/financial data.
- Add strict validation for asset uploads and evidence files.
- Implement first-class asset transfer, maintenance, disposal, and verification workflows.
- Add direct URL/IDOR regression tests for every role boundary.

## B. Detailed Findings

### 1. Asset policy uses the wrong ownership field

**Area:** Authorization / MDA data isolation  
**Issue:** Ministry asset access compares `Asset.ministry_id` to `User.id`.  
**Risk level:** Critical  
**Evidence from app/code:** `app/Policies/AssetPolicy.php:18`  
**Why it matters:** A ministry user can be denied access to their own assets, or accidentally authorized if their user ID matches a ministry ID. In a public-sector system, cross-MDA isolation must be deterministic.  
**Recommended fix:** Compare against `user.ministry_id`, and add tests for own-MDA access and cross-MDA direct URL denial.

```php
if ($user->hasRole('ministry-admin')) {
    return $asset->ministry_id === $user->ministry_id;
}
```

### 2. Asset request policy has the same MDA ownership bug

**Area:** Authorization / request confidentiality  
**Issue:** Ministry request access compares `AssetRequest.ministry_id` to `User.id`.  
**Risk level:** Critical  
**Evidence from app/code:** `app/Policies/AssetRequestPolicy.php:29`  
**Why it matters:** Asset requests include procurement needs, justification, costs, and approval status. These should not leak across MDAs or become inaccessible to their rightful MDA.  
**Recommended fix:** Compare `request.ministry_id` to `user.ministry_id`.

```php
if ($user->hasRole('ministry-admin')) {
    return $request->ministry_id === $user->ministry_id;
}
```

### 3. Ministry request creation accepts client-controlled `ministry_id`

**Area:** Access control / audit accountability  
**Issue:** `StoreAssetRequestFormRequest` validates `ministry_id` as a required submitted field, and the controller passes validated request data into the DTO.  
**Risk level:** Critical  
**Evidence from app/code:** `app/Http/Requests/StoreAssetRequestFormRequest.php:17`, `app/Http/Controllers/Ministry/AssetRequestController.php:49`  
**Why it matters:** A ministry user may be able to create a request for another MDA by changing form data. This breaks accountability and audit reliability.  
**Recommended fix:** Remove `ministry_id` from user-submitted validation for ministry users. Set it server-side from `Auth::user()->ministry_id`.

```php
$validated = $request->validated();
$validated['ministry_id'] = Auth::user()->ministry_id;
```

### 4. Public registration is open

**Area:** Authentication / onboarding  
**Issue:** Public register routes are enabled, and new users are logged in immediately after registration.  
**Risk level:** Critical  
**Evidence from app/code:** `routes/auth.php:15`, `routes/auth.php:18`, `app/Http/Controllers/Auth/RegisteredUserController.php:46`  
**Why it matters:** Government systems should not allow arbitrary self-registration into an internal asset platform. Even if new users have no role, unmanaged accounts increase attack surface and operational confusion.  
**Recommended fix:** Disable public registration in production. Use super-admin provisioning, invite-only registration, or HR/MDA-backed onboarding with approval.

### 5. Inactive users are not blocked during authentication

**Area:** Authentication / account lifecycle  
**Issue:** `is_active` exists and is cast on `User`, but `LoginRequest` uses `Auth::attempt()` with only email and password.  
**Risk level:** High  
**Evidence from app/code:** `app/Models/User.php:51`, `app/Models/User.php:76`, `app/Http/Requests/Auth/LoginRequest.php:44`  
**Why it matters:** Suspended staff, former contractors, or disabled supplier accounts may still authenticate.  
**Recommended fix:** Include `is_active` in the authentication check or reject inactive users after credential validation and immediately invalidate the session.

### 6. Test suite currently fails before running

**Area:** QA / CI readiness  
**Issue:** Pest test-case binding is applied globally and again in at least one feature test.  
**Risk level:** Critical  
**Evidence from verification:** `php artisan test` failed with `Test case [Tests\TestCase] can not be used... already uses the test case [Tests\TestCase]`. Evidence files: `tests/Pest.php:14`, `tests/Feature/AssetRequestCreateTest.php:10`  
**Why it matters:** A public-sector production system needs a passing automated test suite before deployment. Current tests cannot provide release confidence.  
**Recommended fix:** Remove duplicate per-file `uses(Tests\TestCase::class, RefreshDatabase::class)` declarations where `tests/Pest.php` already applies them.

### 7. Activity log table exists but service writes to log files

**Area:** Audit trail / government accountability  
**Issue:** `ActivityLogService` writes events to `Log::channel('activity')`, while the database migration defines an `activity_logs` table with subject and metadata columns.  
**Risk level:** High  
**Evidence from app/code:** `app/Services/ActivityLogService.php:37`, `database/migrations/2026_01_29_224419_create_activity_logs_table.php:14`  
**Why it matters:** File logs are harder to query, retain, secure, reconcile, and expose to auditors. Asset movement, approvals, user changes, imports, exports, and disposals need durable audit records.  
**Recommended fix:** Persist audit logs to the database with actor, subject, action, before/after values, IP, user agent, correlation ID, and timestamp. Keep log-channel writes as secondary observability only.

### 8. Asset import maps fields that do not match the asset model/schema

**Area:** Data integrity / bulk onboarding  
**Issue:** `AssetsImport` creates `category_id`, `description`, `location`, and `serial_number`, while assets use `asset_category_id`; serial/location are not core fillable asset columns.  
**Risk level:** High  
**Evidence from app/code:** `app/Imports/AssetsImport.php:31`, `app/Imports/AssetsImport.php:38`, `app/Imports/AssetsImport.php:39`, `app/Models/Asset.php`  
**Why it matters:** Bulk imports can silently lose important data or fail. For government onboarding, import reliability is central because historical registers may be migrated in batches.  
**Recommended fix:** Align import fields to the real schema. Store serial/location as dedicated columns or validated custom attributes. Add import tests.

### 9. Asset upload validation is incomplete

**Area:** File upload security  
**Issue:** Asset create/update handles `$request->file('files')`, but the asset request validation does not define `files` or `files.*` rules.  
**Risk level:** High  
**Evidence from app/code:** `app/Http/Controllers/Admin/AssetController.php:111`, `app/Http/Controllers/Admin/AssetController.php:179`, `app/Http/Requests/StoreAssetRequest.php`  
**Why it matters:** Upload endpoints are common attack paths. The app should constrain MIME type, size, count, extension, storage disk, and access rules.  
**Recommended fix:** Add validation for `files`, `files.*`, allowed document/image types, maximum file size, maximum count, and private/evidence storage policy.

### 10. Asset media deletion should be more explicitly scoped

**Area:** File ownership / IDOR  
**Issue:** Asset update accepts `deleted_media` IDs and deletes matching asset media records.  
**Risk level:** Medium/High  
**Evidence from app/code:** `app/Http/Controllers/Admin/AssetController.php:188`  
**Why it matters:** Media deletion must be tightly scoped to the current asset and collection. Client-submitted IDs should never be trusted broadly.  
**Recommended fix:** Validate `deleted_media.*` as integers and delete only media where `model_type`, `model_id`, and collection match the current asset.

### 11. Asset update validation treats asset tag as create-only unique

**Area:** Data integrity / user flow  
**Issue:** `StoreAssetRequest` uses `unique:assets,asset_tag` without ignoring the current asset on update.  
**Risk level:** Medium  
**Evidence from app/code:** `app/Http/Requests/StoreAssetRequest.php:21`  
**Why it matters:** Editing an existing asset can fail if the asset keeps its own tag.  
**Recommended fix:** Use separate create/update form requests or `Rule::unique('assets', 'asset_tag')->ignore($asset->id)`.

### 12. Validation messages reference missing rules

**Area:** Validation correctness  
**Issue:** Messages exist for `purchase_date.before_or_equal` and `salvage_value.lte`, but those rules are not applied.  
**Risk level:** Medium  
**Evidence from app/code:** `app/Http/Requests/StoreAssetRequest.php:41`, `app/Http/Requests/StoreAssetRequest.php:42`  
**Why it matters:** Future purchase dates and salvage values greater than purchase cost can distort depreciation and reporting.  
**Recommended fix:** Add `before_or_equal:today` for purchase date and `lte:purchase_cost` for salvage value.

### 13. Ministry asset filter references inconsistent status fields

**Area:** Data correctness / reporting  
**Issue:** Ministry asset filtering uses both `status_id` and `status`, but the asset table uses `status`.  
**Risk level:** Medium  
**Evidence from app/code:** `app/Http/Controllers/Ministry/AssetController.php:38`  
**Why it matters:** Status filters may return incorrect results or no results.  
**Recommended fix:** Remove `status_id` and use the enum-backed `status` column consistently.

### 14. Maintenance dashboard count uses wrong status value

**Area:** Reporting accuracy  
**Issue:** One ministry dashboard stat checks `status = maintenance`, while the app status enum uses `under_maintenance`.  
**Risk level:** Medium  
**Evidence from app/code:** `app/Http/Controllers/Admin/MinistryController.php:58`, `app/Enums/AssetStatus.php`  
**Why it matters:** Maintenance counts can be wrong, reducing trust in dashboards.  
**Recommended fix:** Use `AssetStatus::Maintenance` or the exact `under_maintenance` value consistently.

### 15. Approver role naming is inconsistent

**Area:** RBAC / test reliability  
**Issue:** Routes and seeders use `approver`, while tests assign `approver-1` and `approver-2`.  
**Risk level:** Medium  
**Evidence from app/code:** `routes/web.php:104`, `database/seeders/RolesAndPermissionsSeeder.php:82`, `tests/Feature/AuthorizationPolicyTest.php:35`, `tests/Feature/AuthorizationPolicyTest.php:38`  
**Why it matters:** Tests may not reflect production roles, and role changes can accidentally break approval access.  
**Recommended fix:** Standardize role names. If approval levels matter, model level assignment separately from role identity.

### 16. Reports load full datasets into memory

**Area:** Performance / scalability  
**Issue:** Several report actions use `get()` instead of pagination or queued export jobs.  
**Risk level:** Medium  
**Evidence from app/code:** `app/Http/Controllers/Admin/ReportController.php:27`, `app/Http/Controllers/Admin/ReportController.php:43`, `app/Http/Controllers/Admin/ReportController.php:59`, `app/Http/Controllers/Admin/ReportController.php:73`  
**Why it matters:** A state-wide system can contain large asset registers. Full in-memory reports can become slow or crash under load.  
**Recommended fix:** Paginate interactive reports and queue large PDF/Excel exports.

### 17. No API layer is currently registered

**Area:** API readiness / integrations  
**Issue:** `bootstrap/app.php` registers web routes, console routes, and health route, but no API route file is configured.  
**Risk level:** Medium  
**Evidence from app/code:** `bootstrap/app.php:8`  
**Why it matters:** If external integrations, mobile clients, or inter-agency systems are expected, API versioning, authentication, throttling, response contracts, and documentation are not ready.  
**Recommended fix:** If APIs are required, add versioned API routes with auth middleware, policies, resource responses, pagination, validation, rate limits, and OpenAPI documentation.

### 18. Asset lifecycle workflows are incomplete

**Area:** Asset management standards / ISO 55000 readiness  
**Issue:** The app tracks basic asset status but does not implement first-class transfer, maintenance, disposal, physical verification, missing/damaged, or reconciliation workflows.  
**Risk level:** High  
**Evidence from app/code:** Status enum exists in `app/Enums/AssetStatus.php`; no transfer, maintenance, disposal, or verification modules were found as first-class controllers/models/migrations.  
**Why it matters:** Public-sector asset accountability depends on lifecycle traceability, evidence, approvals, and immutable history.  
**Recommended fix:** Add dedicated modules for transfer, maintenance, disposal, audit verification, reconciliation, custodian assignment, and evidence upload.

## C. Missing Features Checklist

- [ ] Invite/admin-only onboarding
- [ ] Inactive account login blocking
- [ ] MFA for super admin, admin, approvers, and finance/procurement roles
- [ ] Full account lockout policy beyond login throttle messaging
- [ ] Role model for State Admin
- [ ] Role model for MDA Admin
- [ ] Role model for Asset Officer
- [ ] Role model for Auditor
- [ ] Role model for Viewer/Read-only User
- [ ] Role model for Maintenance Officer
- [ ] First-class asset transfer workflow
- [ ] Transfer request approval
- [ ] Source and destination MDA history
- [ ] Custodian assignment
- [ ] Maintenance request workflow
- [ ] Maintenance cost tracking
- [ ] Maintenance history
- [ ] Disposal request workflow
- [ ] Disposal approval
- [ ] Disposal evidence upload
- [ ] Prevention of unauthorized edits to disposed assets
- [ ] Physical asset verification
- [ ] Missing/damaged asset reporting
- [ ] Audit reconciliation reports
- [ ] Asset history timeline
- [ ] Durable database audit trail
- [ ] Export audit logging
- [ ] Backup and restore runbook
- [ ] Retention and deletion policy
- [ ] Secure evidence/document storage policy
- [ ] Versioned API layer if integrations are required
- [ ] OpenAPI/API documentation if integrations are required
- [ ] Queued large exports
- [ ] Production CI/CD evidence

## D. Security Checklist

| Item | Status |
| --- | --- |
| Login route protected from brute force | Pass |
| Password hashing | Pass |
| Strong password defaults | Pass |
| Session regeneration on login | Pass |
| Logout invalidates session | Pass |
| Authenticated route grouping | Pass |
| Verified-email route grouping | Pass |
| UUID route keys | Pass |
| Public registration disabled | Fail |
| Inactive user login blocking | Fail |
| MFA readiness | Fail |
| Correct MDA policy ownership checks | Fail |
| Ministry request server-side MDA binding | Fail |
| Role-based route middleware | Partial pass |
| Fine-grained policies across all actions | Partial |
| Direct URL cross-MDA tests | Partial/fail |
| Asset upload validation | Fail |
| Evidence/private file storage policy | Partial/fail |
| Durable audit logging | Fail |
| Export audit logging | Fail |
| API rate limiting | Not applicable/no API layer |
| API response consistency | Not applicable/no API layer |
| Session encryption production enforcement | Partial/env-dependent |
| Secure cookie production enforcement | Partial/env-dependent |
| Secrets exposure check | Not fully verified |
| Debug production posture | Not fully verified |

## E. Asset Management Standards Checklist

| Requirement | Status |
| --- | --- |
| Asset registration | Pass |
| Asset classification | Pass |
| Unique asset tag | Pass |
| MDA assignment | Pass |
| Location tracking | Partial/import template suggests it, but schema handling is unclear |
| Custodian assignment | Fail |
| Asset condition/status | Partial |
| Acquisition details | Partial/pass |
| Supplier linkage | Partial |
| Document/evidence upload | Partial |
| Asset lifecycle history | Fail |
| Transfer workflow | Fail |
| Maintenance workflow | Fail |
| Disposal workflow | Fail |
| Verification/audit workflow | Fail |
| Missing/damaged reporting | Fail |
| Depreciation fields | Pass/partial |
| Depreciation processing | Partial |
| Search/filter | Partial/pass |
| Pagination | Partial/pass |
| Excel/PDF export | Partial/pass |
| Dashboard analytics | Partial/pass |
| Audit trail | Fail as durable control |
| ISO 55000 lifecycle readiness | Partial/fail |
| Public-sector accountability readiness | Partial/fail |

## F. Graceful Failure Checklist

| Failure Case | Status |
| --- | --- |
| Invalid login | Pass |
| Login throttling | Pass |
| Expired session redirect | Partial/default Laravel behavior |
| Unauthorized route access | Partial |
| Cross-MDA direct URL handling | Fail/partial due policy bugs |
| Validation errors on forms | Partial/pass |
| Asset upload failure | Partial |
| Import validation failure | Partial |
| Procurement invalid status transition | Partial |
| PDF generation failure | Partial |
| Excel export failure | Partial |
| Empty asset/request states | Partial |
| Large report failure | Fail/partial |
| Server error user experience | Partial/default Laravel behavior |
| Developer error logging | Partial |
| Sensitive error detail suppression | Env-dependent |

## G. Recommended Architecture Improvements

### Backend

- Fix policy ownership checks and add regression tests for every role boundary.
- Make ministry/MDA identity server-derived for all MDA-scoped operations.
- Disable public registration in production.
- Enforce `is_active` at login.
- Add MFA support for privileged roles.
- Add dedicated Laravel policies for assets, asset requests, approvals, procurement, suppliers, ministries, reports, users, imports, exports, and audit logs.
- Convert activity logging into durable database audit events.
- Add immutable audit records for create, update, delete, approve, reject, fulfill, transfer, dispose, import, export, login, logout, and role changes.
- Split asset create and update validation requests.
- Harden upload validation and storage.
- Fix import mapping and store serial/location consistently.
- Add queued jobs for large exports.
- Add first-class transfer, maintenance, disposal, and verification modules.

### Frontend and UX

- Add role-aware navigation/action guards so users only see actions they can perform.
- Add clear permission-denied screens for restricted MDA/admin actions.
- Add empty states for no assets, no requests, no approvals, no reports, and no procurement orders.
- Add confirmation modals for destructive actions.
- Add clear upload validation feedback.
- Add import preview and import error-row download.
- Add audit/history panels on asset, request, procurement, and user detail pages.
- Add user-friendly expired-session handling.

### Database and Infrastructure

- Add indexes for common report filters and search fields.
- Add tables for transfers, maintenance records, disposals, verification exercises, asset custodians, and asset history events.
- Add retention policy for soft-deleted records and uploaded evidence.
- Add backup and restore runbooks.
- Add production secret management.
- Add production environment validation for `APP_DEBUG=false`, secure cookies, HTTPS, trusted proxies, and session encryption.
- Add observability for login failures, authorization denials, import failures, export jobs, queue failures, approval bottlenecks, and asset lifecycle events.

## H. Launch Readiness Verdict

**Current state:** Not production-ready.

The application can continue as a development/UAT foundation, but it should not launch with real Ogun State asset records yet. The app needs access-control fixes, public onboarding lockdown, durable audit logging, lifecycle modules, import repair, upload hardening, and a passing automated test suite before production use.

## Verification Performed

Repository inspection:

- Reviewed route structure in `routes/web.php` and `routes/auth.php`.
- Reviewed authentication request handling in `app/Http/Requests/Auth/LoginRequest.php`.
- Reviewed registration/logout flow in `app/Http/Controllers/Auth`.
- Reviewed user model, roles/permissions seeder, and policies.
- Reviewed asset, request, approval, procurement, supplier, report, import, export, and dashboard-related code.
- Reviewed migrations for assets, requests, approvals, activity logs, UUIDs, ministries, and soft deletes.
- Reviewed selected feature tests and Pest configuration.

Commands run:

- `rg --files`
- `php artisan test`
- Multiple targeted `rg` and `Get-Content` inspections
- `php -l app\Http\Controllers\Admin\AssetController.php`

Verification result:

- `php -l app\Http\Controllers\Admin\AssetController.php` passed.
- `php artisan test` failed before running tests due duplicate Pest test-case binding.

Failure observed:

```text
Test case [Tests\TestCase] can not be used. The folder [C:\Users\PLAYHOUSE\Herd\ogsg-asset-management\tests\Feature\AssetRequestCreateTest.php] already uses the test case [Tests\TestCase].
```

Note:

This audit is based on local static inspection and one attempted test run. It did not include browser-based UAT, dependency vulnerability scanning, production `.env` review, load testing, or a live deployment security review.

