Skip to content

Latest commit

 

History

10 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FileAudit

FileAudit is a small internal PHP dashboard for storing and searching Windows file audit events. It uses plain PHP, MariaDB, PDO, and no external dependencies.

FileAudit dashboard

Setting up for the first time? Start at Install on Debian. Updating a server that is already running FileAudit? See Upgrading an existing install instead.

Upgrading an existing install

FileAudit has no version file, so work out what you need from the database itself. Run each check below; if it returns nothing, apply that step. The steps are cumulative and must be applied in order.

Back up first:

mysqldump -u fileaudit_user -p fileaudit > ~/fileaudit-backup-$(date +%F).sql

Run every command below from the normal shell prompt, not from inside mysql>.

1. DNS URL log table (added 2026-07-15)

mysql -u fileaudit_user -p fileaudit -e "SHOW TABLES LIKE 'dns_queries';"

No rows means you need it:

mysql -u fileaudit_user -p fileaudit < database/dns-schema.sql

2. resolved_action column (added 2026-07-31)

mysql -u fileaudit_user -p fileaudit -e "SHOW COLUMNS FROM audit_events LIKE 'resolved_action';"

No rows means you need it:

ALTER TABLE audit_events ADD COLUMN resolved_action VARCHAR(32) NULL AFTER raw_json;
ALTER TABLE audit_events ADD INDEX idx_resolved_action (resolved_action, time_created);

See Deleted vs Replaced for what this column does.

3. Performance migration (added 2026-08-25)

mysql -u fileaudit_user -p fileaudit -e "SHOW INDEX FROM audit_events WHERE Key_name = 'idx_path_lookup';"

No rows means you need it. Run this before deploying the new PHP. The updated code no longer filters legacy .tmp rows at read time, so deploying first would leave them visible in the UI until this migration purges them.

mysql -u fileaudit_user -p fileaudit < database/migrations/2026-08-25-performance.sql

On a large table the index rebuild can take tens of minutes, so run it inside tmux or screen. The file is idempotent and safe to re-run if it stops partway. Performance and retention explains what it changes.

4. Deploy the PHP

Copy app/, public/ and bin/ across together. app/functions.php and the pages under public/ changed in the same commit and depend on each other's function signatures, so a half-finished copy produces fatal errors. Copying app/functions.php last keeps that window as short as possible.

config/config.php needs no changes. Every setting added since the initial release has a built-in default, so an older config file still works. Compare it against config/config.example.php only if you want to tune retention, result-count limits or resolver batch sizes.

5. Verify

From the project root:

find . -name '*.php' -exec php -l {} \; | grep -v 'No syntax errors'
php bin/maintain.php status
php bin/maintain.php resolve

The first prints nothing if every file is valid. status should report a sane row count and unresolved deletion rows trending to 0 after resolve runs. Then load each page in the browser, and confirm the collector's next run returns resolved and pruned in its JSON response.

Install on Debian

sudo apt update
sudo apt install apache2 mariadb-server php8.4 php8.4-mysql libapache2-mod-php8.4

If your Debian release does not provide PHP 8.4 packages, use the default supported PHP packages instead:

sudo apt install apache2 mariadb-server php php-mysql libapache2-mod-php

Enable Apache rewrite only if your environment needs it. This app does not require pretty routing.

Create the database

Log in to MariaDB as root:

mysql -u root -p

Then run:

CREATE DATABASE fileaudit CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'fileaudit_user'@'localhost' IDENTIFIED BY 'change_this_password';
GRANT ALL PRIVILEGES ON fileaudit.* TO 'fileaudit_user'@'localhost';
FLUSH PRIVILEGES;

Import the schema:

mysql -u fileaudit_user -p fileaudit < database/schema.sql

Run that import command from the normal Debian shell prompt, not from inside the mysql> or MariaDB> prompt. If you are already inside MariaDB, type exit first.

database/schema.sql is for fresh installs only and creates both tables. If you are updating a server that already has data, do not import it — follow Upgrading an existing install instead.

Configure the app

Copy the example config and edit the values:

cp config/config.example.php config/config.php

Create an admin password hash:

php -r "echo password_hash('change_this_admin_password', PASSWORD_DEFAULT), PHP_EOL;"

Put the generated hash in ADMIN_PASSWORD_HASH. Set API_TOKEN to a long random value. If you want to restrict collectors by IP, add addresses to TRUSTED_COLLECTOR_IPS; leave it empty to allow any source IP with the correct token.

Set APP_TIMEZONE to the timezone you want displayed in the dashboard, for example:

'APP_TIMEZONE' => 'Europe/London',

DNS query records have their own retention period. The default is seven days:

'DNS_LOG_RETENTION_DAYS' => 7,

To reduce repeated browsing noise, FileAudit also keeps only one DNS query for the same computer IP and domain within one minute. Change DNS_LOG_DEDUPLICATE_MINUTES in config/config.php to adjust the interval, or set it to 0 to retain every query.

Collector timestamps are stored in UTC; the dashboard converts them for display.

Apache

Point the Apache virtual host document root at the public directory, not the repository root.

Create or edit a site file under /etc/apache2/sites-available/:

sudo nano /etc/apache2/sites-available/fileaudit.conf

Example if the project is installed at /var/www/fileaudit:

<VirtualHost *:80>
    ServerName fileaudit.internal
    DocumentRoot /var/www/fileaudit/public

    <Directory /var/www/fileaudit/public>
        Require all granted
        AllowOverride None
    </Directory>
</VirtualHost>

If you copied the project to /var/www/html, use this path instead:

DocumentRoot /var/www/html/public

<Directory /var/www/html/public>
    Require all granted
    AllowOverride None
</Directory>

Enable the site and reload Apache:

sudo a2ensite fileaudit.conf
sudo systemctl reload apache2

If Apache still shows the default page, disable the default site and reload again:

sudo a2dissite 000-default.conf
sudo systemctl reload apache2

fileaudit.internal only works if your DNS or hosts file resolves that name to the server. For local testing on the server itself, use localhost or 127.0.0.1.

Test the API

Run this from the normal Debian terminal after Apache, MariaDB, config/config.php, and the schema are in place. Replace the bearer token with the exact API_TOKEN value from config/config.php.

curl -X POST http://localhost/api/ingest.php \
  -H "Authorization: Bearer replace_this_with_a_long_random_token" \
  -H "Content-Type: application/json" \
  -d '{
    "server_name": "FS01",
    "computer_name": "FS01",
    "event_id": 4663,
    "record_id": 123456,
    "time_created": "2026-06-30T10:15:00Z",
    "username": "j.smith",
    "domain_name": "EXAMPLE",
    "source_ip": "192.0.2.55",
    "object_name": "C:\\Shares\\Finance\\budget.xlsx",
    "action": "Modified",
    "access_mask": "0x2",
    "process_name": "C:\\Windows\\explorer.exe",
    "status": "Success"
  }'

If Apache DocumentRoot points to /var/www/html instead of /var/www/html/public, the test URL would be http://localhost/public/api/ingest.php. The cleaner setup is to point DocumentRoot at the public directory and use http://localhost/api/ingest.php.

Expected first successful response:

{"ok":true,"inserted":1,"duplicates":0,"errors":[]}

Running the same event again should be treated as a duplicate:

{"ok":true,"inserted":0,"duplicates":1,"errors":[]}

If you see Bearer token required, check that the curl command starts with curl -X POST and includes exactly one authorization header like:

-H "Authorization: Bearer replace_this_with_a_long_random_token"

Deletion correlation and Windows collector

FileAudit supports Windows event IDs 4656 and 4659 and stores correlation fields used to match deletion confirmation events with nearby path-bearing events.

Required event IDs:

  • 4656 - A handle to an object was requested
  • 4659 - A handle to an object was requested with intent to delete
  • 4660 - An object was deleted
  • 4663 - An attempt was made to access an object
  • 4670 - Permissions on an object were changed
  • 5145 - Detailed file share access check

Event 4660 often confirms deletion but does not include the file or folder path. Event 4659 is a strong delete-intent signal. FileAudit stores handle_id and logon_id so the event detail page can show related 4656, 4659, or 4663 events from the same server within plus/minus 2 minutes.

Database schema

Fresh installs use database/schema.sql, which already includes the correlation fields, the resolved_action column and every index. No separate migration is required.

Existing installs need those applied in order — see Upgrading an existing install.

Deleted vs Replaced

A confirmed 4660 only means Windows deleted an object; it does not mean the path is gone for good. Many applications save by deleting the original file and immediately creating a new file at the same path (an atomic "safe save"), which is indistinguishable from a real removal by looking at a single event. FileAudit resolves this once enough time has passed for correlated events to arrive (a few minutes) and stores the outcome in resolved_action:

  • Deleted - a 4660 confirmed the delete, and the path was not created or modified again within 2 minutes.
  • Replaced - a 4660 confirmed the delete, but the same path was created or modified again within 2 minutes, so the file was not actually left missing.
  • DeleteRequested - no confirming 4660 was found for the handle; this is a delete-intent/access signal only.

4659 alone is never displayed as Deleted on its own - it only becomes Deleted or Replaced once a real, handle-correlated 4660 is found.

This resolution runs as background work during collector ingest (resolve_pending_deletions() in app/functions.php, called from public/api/ingest.php), in slices bounded by RESOLVE_BATCH_SIZE and RESOLVE_MAX_SECONDS. It no longer runs from page loads, where it cost several hundred serial queries before any HTML was sent.

One consequence is visible in the UI: a newly collected 4663/4659 row shows as DeleteRequested until the next collector run classifies it - roughly the 3 minute correlation grace window plus the collector interval. That is the honest state, since nothing has confirmed the delete yet, and it settles to Deleted or Replaced on its own.

To drain a large existing backlog in one go:

php bin/maintain.php resolve

Performance and retention

audit_events is append-only and high volume, so the read path is built to stay bounded as it grows.

  • Counts are capped. List pages stop counting at EVENT_COUNT_LIMIT (default 10,000) and display the total as e.g. 10,000+. An exact COUNT(*) is proportional to table size and ran on every page view. Raise the limit to trade page speed for an exact total.
  • List queries select only displayed columns. raw_json is a full copy of every event, averaging around 1 KB and roughly 60% of the table's data size. It is only ever displayed on the single-event detail page, so fetching it for 100 list rows pulled it through the buffer pool and into PHP memory for nothing.
  • Deletion resolution is background work, driven by the collector rather than by page loads.
  • Retention is opt-in. AUDIT_LOG_RETENTION_DAYS defaults to 0, meaning keep everything. Set it to a number of days and old rows are removed in bounded chunks during ingest. DNS records are retained separately via DNS_LOG_RETENTION_DAYS.

Because retention deletes audit data permanently, it stays disabled until you choose a window.

Maintenance CLI

php bin/maintain.php status      # backlog size, table size, retention setting
php bin/maintain.php resolve     # drain the resolved_action backlog
php bin/maintain.php prune       # apply AUDIT_LOG_RETENTION_DAYS now
php bin/maintain.php purge-tmp   # one-off removal of legacy .tmp rows

The collector already drives resolve and prune on every run, so these are for backfills, the one-off .tmp purge, or running maintenance from cron independently of collector activity.

If pages are still slow

Check whether the working set fits in memory - this is usually the cliff:

SELECT ROUND((data_length + index_length) / 1048576) AS total_mb
FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name = 'audit_events';

SHOW VARIABLES LIKE 'innodb_buffer_pool_size';

If the table plus its indexes greatly exceeds the buffer pool, every scan is hitting disk. Either raise innodb_buffer_pool_size (a dedicated database server can usually give it 50-70% of RAM) or set a retention window so the table stops growing.

Windows Server auditing setup

Enable Advanced Audit Policy on the file server:

  • Object Access > Audit File System: Success and Failure
  • Object Access > Audit File Share: Failure only, optional
  • Avoid enabling successful read/list auditing unless specifically required

Folder SACL guidance:

  1. Open the file share folder properties.
  2. Go to Security > Advanced > Auditing.
  3. Add narrow auditing entries for Domain Users, a target security group, or Everyone.

Recommended Success permissions:

  • Create files / write data
  • Create folders / append data
  • Write attributes
  • Write extended attributes
  • Delete
  • Delete subfolders and files
  • Change permissions
  • Take ownership

Recommended Failure permissions:

  • Write data
  • Delete
  • Delete subfolders and files
  • Change permissions
  • Take ownership

Do not audit successful Read data/List folder unless you really need it. Successful read/list auditing can generate very large event volumes.

Security log size

Start with a 1 GB Security log for small sites, or 2 GB if the server is busy. Use overwrite-as-needed retention.

wevtutil sl Security /ms:1073741824
wevtutil gl Security

Collector install

The collector is in the collector directory and works with Windows PowerShell 5.1. PowerShell 7 is not required.

On the Windows file server:

  1. Copy the collector folder to the server.
  2. Copy collector-config.example.ps1 to collector-config.ps1.
  3. Set $ApiUrl to your FileAudit API URL.
  4. Set $ApiToken to the same value as API_TOKEN in config/config.php.
  5. Run PowerShell as Administrator.
  6. Run FileAuditCollector.ps1 manually first.
  7. Check C:\ProgramData\FileAudit\collector.log.
  8. Confirm events arrive in the dashboard.
  9. Create the Scheduled Task.

Default collector config:

$ApiUrl = "https://fileaudit.local/api/ingest.php"
$ApiToken = "CHANGE_ME"
$ServerName = $env:COMPUTERNAME
$EventIds = @(4656,4659,4660,4663,4670,5145)
$BatchSize = 100
$MaxEventsPerRun = 2000
$FirstRunLookbackMinutes = 30
$StatePath = "C:\ProgramData\FileAudit\state.json"
$LogPath = "C:\ProgramData\FileAudit\collector.log"

The collector stores last_record_id in the state file and only processes events with a higher RecordId. On first run, when no state file exists, it reads only recent events from the last $FirstRunLookbackMinutes minutes so it does not import the entire Security log.

State is only updated after the API accepts all posted batches.

For HTTPS with internal certificates, install a trusted certificate on the Windows server. Do not disable certificate validation by default. For early lab testing, HTTP on an internal network is simpler.

If the collector reports no matching Security events but you can find those event IDs in Event Viewer, check the event times and the collector state file. They may be older than $FirstRunLookbackMinutes, or C:\ProgramData\FileAudit\state.json may already contain a last_record_id higher than those events.

Useful manual checks:

Get-WinEvent -FilterHashtable @{LogName="Security"; Id=4656,4659,4660,4663,4670,5145; StartTime=(Get-Date).AddMinutes(-30)} -MaxEvents 20 |
    Select-Object TimeCreated, Id, RecordId, ProviderName

Get-Content C:\ProgramData\FileAudit\state.json

For initial testing, you can temporarily increase $FirstRunLookbackMinutes in collector-config.ps1, or delete C:\ProgramData\FileAudit\state.json and rerun the collector after generating fresh test activity.

Scheduled task

Review collector/install-scheduled-task.example.ps1 and edit the script path before using it. It demonstrates a task that runs every 5 minutes using:

powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\Path\To\FileAuditCollector.ps1"

The example uses SYSTEM. You can instead use a dedicated service account if that account can read the local Security event log.

DNS URL Log collector

The URL Log menu records DNS queries, so it shows the computer IP address and requested domain name. DNS cannot provide the full URL path, such as /reports/page.html.

The separate DnsLogCollector.ps1 reads C:\dnslog\dnslog.txt on the DNS server and uses the same collector-config.ps1, API token, and server name as FileAuditCollector.ps1. It keeps its own read-position state in dns-state.json and writes diagnostics to dns-collector.log.

  1. Copy the collector directory to the DNS server.
  2. Add the DNS settings from collector-config.example.ps1 to its shared collector-config.ps1, especially $DnsApiUrl and $DnsLogPath.
  3. Run DnsLogCollector.ps1 manually as an account allowed to read the DNS debug log.
  4. Confirm records appear under URL Log.
  5. Review and run install-dns-collector-scheduled-task.example.ps1 to run it every five minutes.

The built-in parser targets the standard Windows DNS debug-log query lines containing UDP Rcv or TCP Rcv. Other DNS products or custom debug formats may need a parser adjustment.

Collector test checklist

Use a temporary audited folder and test:

  • Create a file
  • Modify a file
  • Copy a file into the share
  • Rename a file
  • Move a file inside the share
  • Move a file out of the share
  • Delete a file
  • Change permissions on a test folder

Expected interpretation notes:

  • Copies usually appear as Written or Modified.
  • Moves may appear as Deleted, Written, or Modified depending on whether the move stays on the same volume and how the client performs it.
  • Event 4660 confirms deletion but may not contain the path, so FileAudit uses related 4656, 4659, and 4663 events with the same Handle ID and Logon ID for correlation.
  • Event 4659 means Windows requested a handle with intent to delete. This is a request only and can fire without the object ever being removed, so FileAudit stores it as DeleteRequested and never displays it as Deleted on its own. The dashboard only relabels a correlated path-bearing event as Deleted when an actual 4660 confirmation exists for the same Handle ID (and Logon ID, when available) within a 2 minute window.
  • Event 4656 is kept as HandleRequested even when its access list includes DELETE, because Windows can include DELETE among several requested rights for create/write workflows. It is useful for correlation, but noisy for direct deletion classification.
  • Event 4663 with access mask 0x10000 means DELETE access was requested or used successfully. It does not always prove the file was removed, so the collector records it as DeleteRequested.
  • The Deletions page shows Deleted, Replaced, and DeleteRequested events so path-bearing deletion activity remains visible. See "Deleted vs Replaced" above for how Replaced is determined.
  • The collector recognizes both text access names and Windows %% access codes, including %%1537 for DELETE, %%4422 for DeleteChild, %%4417/%%4418 for write/create activity, and %%1539/%%1540 for permission/owner changes.

For deletion reporting, do not rely on 4660 alone. Windows often logs the useful path-bearing signal as 4663 with DELETE access immediately before, or sometimes instead of, a 4660 confirmation. FileAudit displays stored DeleteRequested events as Delete Activity in tables because they are usually the best way to answer who performed deletion-related activity against a specific path.

To reduce application noise, FileAudit ignores paths ending in .tmp during collection/API ingest and hides any existing .tmp rows from dashboard lists. Office lock files containing ~$ are still collected because they can be useful for identifying who opened a document.

About

FileAudit is a small internal PHP dashboard for storing and searching Windows file deletion and modification events. Uses plain PHP, MariaDB, PDO, and no external dependencies.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages