# PRD — fim

Evidence date: 2026-10-15. Source: `git@bitbucket.org:overdrive/fim.git` at `41df15e` (`master`, 2020-04-23).

There is no prior PRD in the source repository or in Silicon Ark. Requirements below are recovered from `fim.cron.php`, `fim.cls.php`, and `fim.config.php`. Provenance is labelled. The mailbox address and the fact that the database password is empty are recorded in `technical-documentation.md` and `business-rules.md`; they are not repeated here.

The letters FIM are never expanded. Calling the product a file-integrity monitor is **Inferred** from the walk, hash, compare, and email behaviour.

## Product purpose

A caller runs `fim.cron.php`. The script walks a directory, writes one MySQL row per file and per directory, and on a later run compares that snapshot with a master baseline. If anything changed, disappeared, or appeared, it emails an HTML report. Links in that report call the same script to ignore a row or to copy it onto the master baseline.

Provenance: **Implemented**. No documented product goal was found.

## Users and roles

No login, role, or permission check exists.

| Actor | What they do | Evidence | Provenance |
|---|---|---|---|
| Operator or cron | Requests `fim.cron.php` with no `Action`, or with `FIM: RESET MASTER` | `fim.cron.php` | Implemented. The scheduler itself is not in git |
| Report reader | Opens Ignore or Update links in the email | `NemoFIM::Compare` | Implemented. Who receives mail is a single configured address |
| Unauthenticated HTTP client | Can call the same `Action` URLs if the script is reachable | `$_REQUEST` in `fim.cron.php` | Implemented. No shared secret |

**Unknown / Requires Confirmation:** who the operator is, and whether the script is exposed on a public URL.

## Integrations

| System | Direction | Trigger | Data | Auth | Failure |
|---|---|---|---|---|---|
| MySQL database `fim` | Outbound from PHP | Every path that records, compares, ignores, updates, or resets | Audit rows | `mysqli_connect` with values in `fim.config.php`. No connect-error check | `mysqli_query` return values are not checked. A failed query is ignored |
| PHP `mail()` | Outbound | `Compare()` when the sum of change, missing, and new counts is greater than 0 | HTML body | Mail headers in `fim.config.php` | Return value of `mail()` is ignored |
| Web redirect | Outbound header | Timer limit inside `Record()` | `Location: http://www.redirect.to.url.com/` | None | `exit` after the header. Under CLI this header does not navigate anywhere |

`NemoFIM::PullData` would read `sysAudit_{env}` from `$DATABASE_SETTINGS[$server_name]` and rewrite it locally through `NemoDatabase`. The cron action `FIM: PULL MASTER DATA` does not call it. The Nemo types are not in this repository. Provenance: **Implemented** as unused code. Whether a host still provides those globals is **Unknown / Requires Confirmation**.

## Workflows

### WF-1 — Record a snapshot

Trigger: `fim.cron.php` with no recognised `Action`, and `sesFile.txt` either missing or its first line not equal to `1`.

1. Config opens MySQL and sets the scan root.
2. If `sesFile.txt` is missing, the script creates it and deletes rows in `sysAudit_{environment}` whose `dtAudit` date is today (`LEFT(NOW(),10)`).
3. `NemoFIM::Record` walks the root. `.` and `..` and other wildcard matches are skipped.
4. For each remaining directory or file it inserts a row, then chmods the path if the mode is not already `0755` (directory) or `0644` (file).
5. If the timer is enabled and elapsed seconds exceed `ProcessMaxRuntime`, it writes pause state and redirects. If the counter is enabled instead and the in-run counter hits `ProcessMaxCount`, it writes pause state and exits without a redirect.
6. If neither limit fires, `sesFile.txt` is left with first line `1` (written at the start of `Record`).

The next request with first line `1` does not record. It follows WF-2.

Provenance: **Implemented**.

### WF-2 — Compare and email

Trigger: default action and `sesFile.txt` first line equals `1`.

1. Delete `sesFile.txt`.
2. `Compare()` loads the latest `dtAudit` in the environment table.
3. Build three result sets against `sysAudit_master`: hash differs on the same path (changes), master path absent from the environment table (missing), environment path absent from master (new).
4. Render up to 50 detail rows per set, plus unbounded summary counts.
5. If the three summary counts sum to more than zero, send the HTML with `mail()`.

Provenance: **Implemented**.

### WF-3 — Accept or ignore from the report

Trigger: query `Action` plus `ID` plus `state`.

- `FIM: IGNORE` with state `CHANGES` or `MISSING` sets that master row's `strHash` to `IGNORE`. `ID=*` is implemented only for `MISSING`.
- `FIM: IGNORE` with state `NEW` inserts a master row copied from the environment row, with `strHash` forced to `IGNORE`. `ID=*` is not special-cased.
- `FIM: UPDATE` with state `CHANGES` copies owner, permission, size, hash, and mtime from the environment row onto the matching master path, and sets `strLastUser` to `CHANGES`. `ID=*` updates every changed path on the latest audit date.
- `FIM: UPDATE` with state `NEW` inserts the environment row into master. `ID=*` inserts every new path on the latest audit date.
- `FIM: UPDATE` with state `MISSING` is an empty branch.
- `FIM: RESET MASTER` deletes every master row, then inserts today's environment rows.
- `FIM: PULL MASTER DATA` does nothing.

Each handled action `die`s with the action name. Provenance: **Implemented**.

## Functional requirements

### FR-001 — Entry script

The system starts at `fim.cron.php`, which includes `fim.config.php` and `fim.cls.php`.

Provenance: **Implemented**. Confidence: high.

### FR-002 — Project and environment names

The audit table suffix and the mail subject use `$siteProjectName` and `$siteEnviroment`. Committed values are `Test_Project` and `qa`. The config comment says the environment is QA, LIVE, or DEV. Only `qa` is assigned.

Provenance: **Implemented** for the two literals. The allowed set is a comment, not a check.

### FR-003 — Scan root

When `$_SERVER["argv"][0]` is set and non-empty, the scan root is that path with `fim.cron.php` and the substring `fim_2.0` removed. Otherwise the scan root is `$_SERVER["DOCUMENT_ROOT"] . "/fim/folder_structure"`.

Provenance: **Implemented**. Which mode production uses is **Unknown / Requires Confirmation**.

### FR-004 — Exclude names

Names matching `$siteWildcards` are not recorded. The same pattern list is applied again as SQL `NOT LIKE` when comparing. Matching rules differ between the walker and SQL. See `business-rules.md` BR-004.

Provenance: **Implemented**.

### FR-005 — Record metadata

For each recorded path the system stores audit time, type `File` or `Dir`, path relative to the scan root, owner, permission octal (last four digits), size, mtime, hash, extension, and `$siteEnviroment` as `strLastUser`.

Directory hash is `sha1(path + owner + permission)`. File hash is `sha1(sha1_file(contents) + owner + permission + size + mtime)`. Directory extension is empty.

Provenance: **Implemented** in `NemoFIM::Save`.

### FR-006 — Normalise permissions during the walk

After a successful existence check, directories that are not mode `0755` are chmodded to `0755`, and files that are not mode `0644` are chmodded to `0644`. The row is inserted before the chmod, so the stored permission is the previous mode.

Provenance: **Implemented**. Whether chmod is an approved product rule or an accidental side effect is **Requires Confirmation**.

### FR-007 — Bound a single run

If `$processUseTimer` is `1`, the walk stops when elapsed seconds exceed `$ProcessMaxRuntime`. Committed values are `1` and `5`. If the timer is off and `$processUseCounter` is `1`, the walk stops when the in-run counter equals `$ProcessMaxCount` (committed `0` and `10`, so the counter is inactive).

On the timer stop, the script sends `Location: http://www.redirect.to.url.com/` and exits. On the counter stop, it exits without that header.

Provenance: **Implemented**. The redirect host is a placeholder literal.

### FR-008 — Resume

Pause state is two lines in `sesFile.txt`: completion flag, then the global counter. A later `Record()` skips save work until the counter passes the stored pause id, and still recurses into directories so the counter stays aligned. PHP sessions are not used. They were removed in `921e87f`.

Provenance: **Implemented**.

### FR-009 — Compare three finding types

Against the latest environment `dtAudit`:

- **Changes:** same `strPath`, hashes differ, master hash is not `IGNORE`.
- **Missing:** master path has no environment row with that path, master hash is not `IGNORE`. This is not limited to the latest audit date on the environment side.
- **New:** environment row on the latest audit date has no master row with that path.

Detail queries use `LIMIT 0,50`. Summary queries do not.

Provenance: **Implemented**.

### FR-010 — Email the report

The body is an HTML document with a summary table and three detail tables (changes, missing, new). Differing compared fields are coloured red. The second row of each pair is labelled `QA` even when `$siteEnviroment` is not `qa`. Mail is sent only when the sum of the three summary counts is greater than zero.

Provenance: **Implemented**.

### FR-011 — Report actions

Ignore, update, and reset behave as WF-3. Links are `$FIMBaseURL` plus `fim.cron.php` and the query string. Committed base is `localhost/fim/` with no scheme.

Provenance: **Implemented**.

### FR-012 — Master reset

`FIM: RESET MASTER` truncates `sysAudit_master` and inserts `SELECT *` from the environment table where the audit date is `CURDATE()`.

Provenance: **Implemented**.

### FR-013 — Schema shipped as comments

`fim.config.php` contains `DROP TABLE IF EXISTS` and `CREATE TABLE` for `sysaudit_master` and `sysaudit_{environment}`. Both `mysqli_query` calls are commented out, so loading the config does not create or drop tables.

Provenance: **Implemented** as inactive SQL. Whether production tables match is **Unknown / Requires Confirmation**.

## Constraints

- One PHP process, one MySQL connection, no queue.
- PHP `max_execution_time` is 1200 seconds and `memory_limit` is `256M`. The application timer defaults to 5 seconds, which is the tighter bound when the timer flag is on.
- `display_errors` is `1`. Reported errors exclude strict, notice, warning, and deprecated.
- There is no automated test.
- Paths and ids are interpolated into SQL. There is no parameter binding.

## Behaviour that looks incomplete

These are observations, not new requirements.

- `$email_message` is appended with `.=` after the lines that used to initialise it were commented out in `41df15e`.
- `FIM: PULL MASTER DATA` is empty, while `PullData()` still expects Nemo globals.
- `$siteBaseURL` is still listed in the `global` statement and is no longer assigned. A block still derives protocol and script name into `$SystemSettings` and does not use them for `$FIMBaseURL`.
- The walker computes `$fileExtension` and `$I_RECURSIVE` and does not use them. `Record()`'s `$result` argument is never filled.
- `Update()` prints the id and state through `print_rr` before running SQL. `Record()` prints every visited path.

## Planned behaviour

No TODO, ticket, or branch describes future work. The commented `_nemo.email.cls.php` include and the unused `PullData` method are remnants, not a stated plan.

## Acceptance notes for a later change

A change to this product can be tested against FR-001 through FR-013 only after a human confirms the open items in `project-plan.md`. The fixture tree is not an oracle: most text files are empty, `FolderOne_Code1.php` contains the word `test`, and `Word1.rtf` is `{\rtf1}`.
