# Business rules — SecOps

Evidence date: 2026-10-04. Source: `git@bitbucket.org:overdrive/secops.git`, `master` at `b6fa27b`.

Rules below are what the code and the SQL seed do. They are not a signed-off policy. Each rule is marked **Documented**, **Implemented**, or **Inferred**. The README states no business rules.

Credential values, the password hash, reCAPTCHA key values, the personal address stored as `company_email`, and the IP addresses inside `activity_logs` are omitted.

## Access

### BR-001 — Sign-in required

**Implemented.** Controllers that extend `MY_Controller` redirect to `login` when `is_logged()` is false. `Login` redirects the other way when the user is already signed in. `Logout` redirects to `login` when the user is not signed in.

### BR-002 — Account must be active

**Implemented.** `Users_model::attempt` returns `not_allowed` unless `status` equals the string `'1'`. The sign-in page then shows `You are not allowed to Login ! Contact Admin`.

**Inferred.** Integer `1` from a strict store would fail this string compare. The seed stores the integer `1` in SQL. PHP/MySQL driver casting was not executed. **Requires Confirmation** if a newly toggled user can sign in.

### BR-003 — Password check

**Implemented.** Stored passwords are `hash('sha256', plaintext)` with no salt and no `password_hash`. Sign-in minimum length is 6. Profile change minimum is 6 and requires the old password. Reset minimum is 5. Create minimum is 6 in the browser form only; `Users::save` does not check length.

### BR-004 — Permission codes gate modules

**Implemented.** `hasPermissions` is true when `role_permissions` contains the signed-in user's `role` id and the code. `ifPermissions` redirects to `errors/permission_denied` otherwise.

Seeded Admin (role 1) has all 18 codes. Seeded Manager (role 2) has user, role, and permission codes only.

**Implemented exceptions:** `Users::change_status`, `Users::check`, `Permissions::checkIfUnique`, and all of `Profile` do not call `ifPermissions`. AdminLTE example routes do not either.

### BR-005 — Sidebar follows the same codes

**Implemented.** `views/includes/nav.php` shows Users, Activity Logs, Roles, Permissions, Backup, and Settings only when the matching list or settings code is held. The AdminLTE example tree is shown to every signed-in user.

### BR-006 — Self-service role change

**Implemented.** `Profile::updateProfile` writes `post('role')` onto the signed-in user. No permission check.

**Requires Confirmation** whether a user may assign themselves the Admin role.

### BR-007 — User delete guard

**Implemented** as written in `Users::delete`: the delete runs only when `$id !== 1` and `$id != logged($id)`; otherwise the method redirects to `/`.

**Inferred.** CodeIgniter passes the id as a string, so `$id !== 1` is true for `"1"`. `logged($id)` looks up a column named with that id, not the current user id. The guard may not protect user 1 or the current user. **Requires Confirmation** by running the action. Do not treat the redirect as proven.

### BR-008 — Status of another user

**Implemented.** `Users::update` copies `status` only when `logged('id')` is not the target id. A user cannot clear their own status through the edit form.

`Users::change_status` has no such check and no permission check. Any signed-in user who can request that URL can set any user's status from the query string.

## Identity and session

### BR-009 — Username or email

**Implemented.** Sign-in and password reset accept either `username` or `email`, minimum length 5, and the value must already exist. The failure text distinguishes an unknown user (`Invalid Username/Email`) from a bad password (`Invalid Password`).

### BR-010 — Uniqueness

**Implemented** in the browser: `users/add.php` and the check endpoint reject an email or username that already exists on another id. `Users::save` and `Users::update` do not enforce uniqueness in PHP. The `users` table has no unique index.

### BR-011 — Session token

**Implemented.** The token is `sha1` of user id, current password hash, and the login time. It must match on each `is_logged()` call. Session lifetime is 7200 seconds, file driver, IP not bound (`config.php`). Remember-me cookies last 7 days and carry the same token.

### BR-012 — Sign-in sets Spanish

**Implemented.** Every successful `login()` calls `setUserlang('es')`. The seed `default_lang` is `en`. The cookie lasts 30 days. **Requires Confirmation** that forcing Spanish is intended.

## Password reset

### BR-013 — Template

**Implemented.** The message body is the `email_templates` row whose `code` is `reset_password`. Shortcodes are replaced by the parser. From address is the company email setting. Subject includes the company name.

### BR-014 — Token lifetime

**Implemented.** The token is stored and later matched exactly. Nothing deletes it because of age. It is cleared when a new password is saved.

**Inferred.** The token is a bcrypt string placed in the query (`login/new_password?token=`). Characters in a bcrypt hash can break or truncate a query string. Delivery of a usable link was not executed. **Requires Confirmation.**

### BR-015 — Reset response always names an address when the user exists

**Implemented.** After a successful reset request the page shows an obfuscated email (half the local part replaced with asterisks). An unknown user is already rejected by `validate_username` before `resetPassword`.

## Organisation settings

### BR-016 — One company record

**Implemented.** Company name and email are keys in `settings`, not a companies table. The page title uses `company_name`. Password-reset mail uses both keys. Seeded name: `AdminPRO`. Seeded timezone: `Asia/Kolkata`. Seeded `login_theme`: `1`. Seeded reCAPTCHA enabled: `0`.

### BR-017 — reCAPTCHA

**Implemented.** Enabled only when the setting equals `'1'`. The secret is read from settings and sent to Google with the client IP. Keys are stored in the database and are present in `main.sql`.

### BR-018 — Email template edit permission mismatch

**Implemented.** Opening the template list requires `email_templates`. Saving a template requires `login_theme`. That code is not in the `master` seed, so a role that can open the editor cannot pass the save check unless a `login_theme` row is added later.

The login-theme actions require the same missing code, and `application/views/settings/login_theme.php` is not in the tree. The settings sidebar does not link a login-theme item.

## Activity

### BR-019 — What is logged

**Implemented.** `Activity_model::add` stores a title string, a user id, and an IP. Call sites include sign-in, sign-out, user create/update/delete, role create/update, permission create/update/delete, settings updates, template updates, and profile changes.

The IP function prefers `HTTP_CLIENT_IP` and forwarding headers over `REMOTE_ADDR`. **Inferred** the stored IP is client-supplied when those headers are present.

### BR-020 — Who can read the log

**Implemented.** List requires `activity_log_list`. Detail requires `activity_log_view`. Manager, in the seed, has neither. Filters are exact equality on `ip_address` and `user`.

## Backup

### BR-021 — Who can export

**Implemented.** `Backup` calls `ifPermissions('backup_db')` in the constructor, so both the page and `exportDB` require it. The seed grants that code to Admin only.

The export is a zip download named `backup-on-{Y-m-d-H-i-s}.zip`. The code also writes that zip to `pathtobkfolder/` relative to the process. That directory is not in the repository.

## Data rules

### BR-022 — User fields

**Implemented.** `users` columns: name, username, email, password, phone, address, last_login, role, reset_token, status, img_type. Create sets status from the posted value cast to int. Update sets status with `post('status')==1`, which is a boolean, and only for a different user.

### BR-023 — Images

**Implemented.** Allowed types gif, jpg, png, jpeg. Overwrite is true. User files are named `{id}.{extension}`. Missing upload on create copies `uploads/users/default.png`.

### BR-024 — Currency display helper

**Implemented.** `currency()` prefixes a dollar sign and a space. No settings key selects a currency. No invoice or order module uses it on `master`.

## Unmerged branch rules

These exist only on `basic_mvc_structure` (`cad6860`). They are **not** rules of `master`.

**Implemented on that branch only.** New permission codes: `view_examples`, `general_dashboard`, `sonarqube_tool`, `nmap_tool`, `fim_tool`, `manage_sites`, `reporting`, `clients`. New role title `User` (id 3). Admin is granted the new codes. Manager is granted dashboard, sites, reporting, and clients. User is granted dashboard, the three tool codes, and sites. Each new controller action redirects when the code is missing and otherwise shows the static dashboard.

No rule on that branch defines what a scan, a site, a client, or a PDF must contain.

## Rules that are not in the product

No documented approval workflow, no data-retention limit, no password-expiry rule, no lockout after failed attempts, no second factor, no organisation membership beyond the single settings row.
