# PRD — SecOps

Evidence date: 2026-10-04.

Source: `git@bitbucket.org:overdrive/secops.git`, `master` at `b6fa27b` (2022-03-30).

There is no prior PRD in Silicon Ark and no product requirements document in the repository. `README.md` is the Bitbucket placeholder. Requirements below are recovered from controllers, models, helpers, views, `main.sql`, and git history. Provenance is labelled. Credential values are omitted.

The repository name is SecOps. The company name stored in the seed is **AdminPRO**. See `project-plan.md`.

## Product purpose

**Implemented.** A single-organisation admin site where a signed-in user manages other users, roles, permission codes, branding settings, one password-reset email template, an activity log, and a database zip download.

**Not implemented on `master`.** Security-operations work (SonarQube, nmap, file-integrity monitoring, sites, clients, PDF reports). Those names exist only on the unmerged branch `basic_mvc_structure` (`cad6860`), and the actions there render the static dashboard. Provenance: **Planned / unmerged stub**. **Requires Confirmation** that this was the product goal.

## Users and roles

Seeded in `main.sql` on `master`:

| Id | Role title | Grants in the dump |
|---|---|---|
| 1 | Admin | Every seeded permission code, including backup, settings, email templates, and activity logs |
| 2 | Manager | User, role, and permission codes only. No activity logs, settings, backup, or email templates |

One user row: name `Administrator`, username `admin`, email `admin@gmail.com`, role 1, status `1`. The password column is a SHA-256 hash. The plaintext is not recorded here.

The unmerged branch adds role id 3, title `User`, and the tool permissions listed in `project-plan.md`. That role is not on `master`.

There is no self-registration route in the application controllers. The AdminLTE sample `adminlte/examples/register` is a static theme page.

**Unknown / Requires Confirmation:** whether the seeded administrator is used outside this dump, and whether further roles exist only in a live database.

## Systems the product talks to

1. MySQL through MySQLi. `application/config/database.php` default group: host `localhost`, database `personal`, user `root`, empty password, driver `mysqli`, debug on when `ENVIRONMENT` is not `production`.
2. Google reCAPTCHA `https://www.google.com/recaptcha/api/siteverify` from `Login::validate_recaptcha`, only when `google_recaptcha_enabled` is `1`. The seed stores `0` plus site and secret keys. Key values are omitted.
3. Outbound email from `Users_model::resetPassword` via the CodeIgniter email library. From address is `company_email` and from name is `company_name`. There is no `application/config/email.php`, so the library default protocol applies. Whether a host MTA exists is **Unknown**.

No SonarQube client, nmap binary, queue, or object store is called from `master`.

## Major workflows

### WF-1 — Sign in

**Implemented** in `Login` (does not extend `MY_Controller`).

1. If database username, hostname, or database name is empty, the request dies with `Database is not configured`.
2. If `is_logged()` is true, redirect to `dashboard`.
3. `index` shows `account/login`. Body class depends on `login_theme`: value `1` uses `login-page login-background`, otherwise `login-page-side login-background`.
4. `check` requires username at least 5 characters and password at least 6. Username must already match a `username` or `email` (`validate_username`). A miss shows `Invalid Username/Email`.
5. If reCAPTCHA is enabled, `g-recaptcha-response` is posted to Google. Failure message: `Google Recaptcha not valid !`.
6. `Users_model::attempt` loads the user by username or email and compares `hash('sha256', password)` to the stored hash.
7. Status not equal to the string `1` returns `not_allowed` and the message `You are not allowed to Login ! Contact Admin`.
8. Hash mismatch returns `invalid_password` and `Invalid Password`.
9. On `valid`, `login` stores session keys `login`, `login_token` (`sha1` of id, password hash, and time), and `logged` (id and time). If `remember_me` is posted, the same values are cookies for 7 days and the session keys are not set.
10. `setUserlang('es')` runs on every successful sign-in, overwriting the language cookie with Spanish for 30 days.
11. `last_login` is written with PHP format `Y-m-d H:m:i` (`m` is month, not minutes). An activity row is inserted. Redirect to `/`.

### WF-2 — Password reset

**Implemented** in `Login::forget`, `reset_password`, `new_password`, `set_new_password`, and `Users_model::resetPassword`.

1. The user posts a username or email that must already exist (same callback as sign-in).
2. A bcrypt `reset_token` is stored (`password_hash` of time concatenated with user id).
3. The email subject is `Reset Your Account Password | {company_name}`.
4. The body is `email_templates.data` for code `reset_password`, parsed with `{company_name}`, `{site_url}`, `{user_id}`, `{user_name}`, `{user_email}`, `{user_username}`, and `{reset_link}`.
5. The page tells the user a link was sent and shows `obfuscate_email` of the address (half the local part replaced with `*`).
6. `new_password` loads the user by `reset_token` query parameter. A missing or unknown token echoes `Invalid Request` and redirects to `login/forget`.
7. `set_new_password` requires the password and confirmation to match. Minimum length here is 5, not 6. The new password is stored as SHA-256 and `reset_token` is cleared. Flash: `New Password has been Updated, You can login now`.

No expiry is checked on `reset_token`. **Implemented.** Whether that is acceptable is **Requires Confirmation**.

### WF-3 — Signed-in shell

**Implemented** in `MY_Controller`.

1. Empty database config logs the user out and dies with `Database is not configured`.
2. Timezone is `setting('timezone')`. Seed value: `Asia/Kolkata`.
3. Language file `basic` loads for `getUserlang()` (cookie `current_lang`, otherwise `default_lang`, seed `en`).
4. If `is_logged()` is false, redirect to `login`.
5. Page title brand is `setting('company_name')`.

`is_logged()` recomputes `sha1(id + password + time)` and compares it to the session or cookie token. A password change invalidates the token.

`Dashboard::index` renders `application/views/dashboard.php`. The stat boxes are fixed copy (150 new orders, 53% bounce rate, 44 user registrations, 65 unique visitors) plus the rest of the AdminLTE sample. **Implemented** as a theme demo, not as live metrics.

### WF-4 — Users

**Implemented** in `Users`. Each action except `check` and `change_status` calls `ifPermissions` with the matching code.

- List, add, view (user plus role plus that user's activity), edit, delete.
- Create stores role, name, username, email, phone, address, status, and SHA-256 password. An uploaded image (gif, jpg, png, jpeg) is saved as `{id}.{ext}` under `uploads/users/`. Otherwise `uploads/users/default.png` is copied to `{id}.png`.
- The add form (`users/add.php`) requires name, email, username, password (min 6), confirm password, and role in the browser. Email and username call `users/check`, which returns the string `false` when the value is already taken (jQuery Validate remote). The controller `save` method does not repeat that check.
- Update writes profile fields. Status is included only when the editor's id is not the target id. A non-empty password is re-hashed. Image upload is optional.
- Delete is refused by the `else` branch when `$id !== 1` is false or `$id != logged($id)`. URI segments arrive as strings, and `logged($id)` treats `$id` as a column name. This was not executed. **Requires Confirmation** whether user 1 or the current user can be deleted.
- `change_status` sets status from the query string `status=true` and echoes `done`. It does not call `ifPermissions`.

### WF-5 — Roles and permissions

**Implemented.**

- Roles have a title. Create and update replace the `role_permissions` rows for the posted permission codes. Update also writes a SHA-256 password onto the role when `password` is posted. The `roles` table has no password column. **Implemented** path; outcome against MySQL was not executed.
- Permissions have a title and a code. `checkIfUnique` returns `false` or `true` as text for the code. Delete removes the row. The activity message uses `$permission`, which is not set in `delete`.

`ifPermissions` redirects to `errors/permission_denied` when `role_permissions` has no row for the signed-in user's role and that code. The sidebar hides links with `hasPermissions` for users, activity, roles, permissions, backup, and settings. AdminLTE example links in `views/includes/nav.php` are not wrapped in `hasPermissions`.

### WF-6 — Settings, templates, backup, profile

**Implemented.**

- General settings (`general_settings`): date format, datetime format, reCAPTCHA on/off, site key, secret key, timezone, default language. The activity title still says `Company Settings Updated`.
- Company settings (`company_settings`): company name and company email.
- Email templates (`email_templates`): list and edit name and HTML body. Code is not updated (the assignment is commented out). `update_email_templates` calls `ifPermissions('login_theme')`, not `email_templates`. The seed has no `login_theme` permission, and there is no `settings/login_theme` view. The methods `login_theme` and `loginthemeUpdate` still exist in `Settings.php`.
- Backup (`backup_db` on the constructor): `index` shows `export`; `exportDB` builds a zip via `dbutil->backup` and `force_download`. It also writes the zip to the relative path `pathtobkfolder/`.
- Profile has no permission code. The signed-in user can update name, username, email, phone, address, and the posted `role`. Password change requires the old password, a match, and length at least 6, then redirects to `login` without an explicit `logout()` call. The token check fails on the next request because the hash changed. Language changes through `profile/change_language/{code}`.

### WF-7 — Activity log

**Implemented.** List requires `activity_log_list`. Optional query filters `ip` and `user` are exact matches. View requires `activity_log_view`. Rows are written by `Activity_model::add` with title, user id (or the signed-in id), and `ip_address()` (forwarded headers before `REMOTE_ADDR`).

## Functional requirements

### FR-001 — Authenticated admin shell

The system shows the dashboard and admin modules only after `is_logged()` succeeds. Otherwise it redirects to `login`.

Evidence: `MY_Controller`, `basic_helper.php` `is_logged`. Implementation: Implemented. Confidence: High.

### FR-002 — Username or email sign-in

The user signs in with a username or email of at least 5 characters and a password of at least 6 characters. The password is compared as unsalted SHA-256. Status must be the string `1`.

Evidence: `Login::check`, `Users_model::attempt`. Implementation: Implemented. Confidence: High.

### FR-003 — Optional reCAPTCHA

When `google_recaptcha_enabled` is `1`, sign-in also requires a successful Google siteverify response.

Evidence: `Login::validate_recaptcha`, settings keys in `main.sql`. Implementation: Implemented, disabled in the seed. Confidence: High.

### FR-004 — Remember me

A posted `remember_me` flag stores login cookies for 7 days instead of the session keys.

Evidence: `Users_model::login`. Implementation: Implemented. Confidence: High.

### FR-005 — Password reset email

A known user receives a message built from the `reset_password` template, containing a link with `reset_token`. Submitting a new password replaces the hash and clears the token.

Evidence: `Users_model::resetPassword`, `Login::set_new_password`, `email_templates` seed. Implementation: Implemented. Confidence: High. Mail delivery depends on the host and is **Unknown**.

### FR-006 — User administration

A user with the matching permission can list, create, view, edit, and delete users, with browser checks for duplicate email and username.

Evidence: `Users.php`, `views/users/`. Implementation: Implemented. Confidence: High. Delete guard behaviour **Requires Confirmation** (see WF-4).

### FR-007 — Role and permission administration

A user with the matching permission can create and edit roles and attach permission codes, and can create, edit, and delete permission codes.

Evidence: `Roles.php`, `Permissions.php`. Implementation: Implemented. Confidence: High.

### FR-008 — Settings and email template

A permitted user can change company name and email, general display and reCAPTCHA settings, and the reset-password template name and body.

Evidence: `Settings.php`. Implementation: Implemented, with the `login_theme` permission mismatch on template save (WF-6). Confidence: High.

### FR-009 — Activity log

A permitted user can list activity rows, filter by user id or IP, and open one row.

Evidence: `Activity_logs.php`, `Activity_model`. Implementation: Implemented. Confidence: High.

### FR-010 — Database backup download

A permitted user can download a zip of the database.

Evidence: `Backup::exportDB`. Implementation: Implemented. The extra write to `pathtobkfolder/` was not executed. Confidence: Medium.

### FR-011 — Own profile

A signed-in user can change their own profile, picture, password, and language cookie without a permission code.

Evidence: `Profile.php`. Implementation: Implemented. The posted `role` is saved. Whether self-service role change is intended **Requires Confirmation**.

### FR-012 — Security tool console

**Not on `master`.** The unmerged branch names clients, sites, SonarQube, nmap, FIM, and PDF reporting and renders the dashboard for each. No requirement in the repository describes expected scan behaviour.

Evidence: `basic_mvc_structure` controllers. Implementation: Stub, unmerged. Confidence: High that the stub exists. Confidence: none that a scan workflow was specified.

## Constraints

- CSRF protection is off (`csrf_protection` = FALSE). Cookie `httponly` and `secure` are FALSE. `encryption_key` is empty. Global XSS filtering is FALSE. Login and user forms still pass some fields through `xss_clean`.
- Passwords are unsalted SHA-256.
- Permission checks are per action and are missing on `Users::change_status` and on profile updates.
- Single database, no tenant id on users.
- UI languages present as files: `en`, `es`, `hi`. Sign-in forces the `es` cookie.

## Non-goals confirmed by absence

No API for other systems, no public registration, no audit export besides the on-screen log and the database zip, no automated tests, no CI config at the repository root.
