FALYS Documentation

Real-time file integrity monitoring and threat response. Everything you need to install, run and tune FALYS.

v2.2.0WindowsLinuxPostgreSQLPython / Flask
Created28 September 2026
BySheeza Alam Khan & Maimoona Shah Khan
Contactfalys.io/contact
Item supportCodeCanyon item page, Support tab

Introduction

FALYS is a self-hosted file integrity monitoring (FIM) system. A lightweight agent watches folders on Windows or Linux endpoints and reports file events to a central dashboard, which scores them for risk, correlates related activity, and emails alerts to whoever should know. All data is stored in PostgreSQL -- there are no data files.

New here? Read Installation & Setup from top to bottom the first time -- the steps build on each other.

What is in the download

FALYS/
|-- Documentation/
|   |-- index.html               <- this help file
|   `-- images/                  <- screenshots used in the help file
|-- FALYS-server/
|   |-- server/                  <- dashboard + API (Flask) and PostgreSQL layer
|   |   |-- .env.example         <- copy to .env and fill in
|   |   |-- build_output/        <- ready-made agent binaries (Windows + Linux)
|   |   |-- dist/                <- server binary is built here (optional)
|   |   |-- engine/              <- risk scoring / correlation engine
|   |   |-- static/, templates/  <- dashboard CSS/JS and HTML pages
|   |   |-- tests/
|   |   `-- server.py            <- Flask app entry point
|   `-- agent/                   <- endpoint agent source + build scripts
|-- Changelog.txt
|-- Licenses.txt                 <- third-party credits
`-- README.txt

Key features

Requirements

Support

Support for this item is provided through the item's Comments / Support tab on CodeCanyon. Please include your operating system, the FALYS version (server/build_output/VERSION.txt) and the relevant console output.

Installation & Setup

This covers getting the FALYS server running, configuring email alerts, logging in, and deploying your first agent. Read it top to bottom the first time -- the steps build on each other.


1. Choose how you'll run the server

First: PostgreSQL

FALYS stores all of its data -- events, agent keys, settings, risk rules, alert history -- in PostgreSQL. There are no data files, so a database has to exist before the server starts. Any PostgreSQL 12+ works (tested on 16), on this machine or another one.

Quickest way to get one (needs Docker):

cd server
docker compose -f docker-compose.postgres.yml up -d

Or use an existing PostgreSQL server -- create an empty database and a role that can create tables in it. The exact commands, connection options and backups are in PostgreSQL Setup.

Then tell FALYS where it is, in server/.env (copy .env.example):

FALYS_DATABASE_URL=postgresql://falys:change-me@localhost:5432/falys

The server creates its tables automatically on first start, and exits with a clear message if it can't reach the database.

Upgrading an older flat-file install? Stop the server and run python migrate_flat_files_to_postgres.py once from server/ -- it imports the old logs/ folder (events keep their ids). See PostgreSQL Setup.

You have two ways to run the server itself. Pick one.

Option A -- Build the server binary once (Windows or Linux)

The server is delivered as source. A server binary has to be built on the operating system it will run on, and this is a one-time step (on a Windows machine with Python 3.9+):

cd server
.\build_server_exe.ps1

Output: server\dist\FalysServer.exe -- self-contained, no Python needed on the machine that runs it. Put your .env (with the database settings) in the same folder as the .exe and run it:

.\FalysServer.exe

The dashboard will be reachable at https://localhost:8088 (see the note on TLS in Section 4).

Linux server binary instead? Run server/build_server_exe_linux.sh once on a Linux machine with Python 3.9+ -- see that script's header comment for details. Output: server/dist/falys-server.

Agent binaries are included. The dashboard's Agents -> Generate & Download button zips a compiled agent binary together with a fresh, per-deployment config.json and the server's cert.pem. Ready-made agent binaries for Windows (FalysAgent.exe) and Linux (falys-agent) are already in FALYS-server/server/build_output/, so the button works immediately for both platforms. They were compiled from the agent source in FALYS-server/agent/ -- see Building the agent binaries if you want to verify or rebuild them.

Option B -- Run from source (Windows, Linux, or macOS)

Requires Python 3.9 or newer.

cd server
python3 -m venv venv
source venv/bin/activate        # Windows: venv\Scripts\activate
pip install -r requirements.txt
python3 server.py

Make sure the database settings are in .env first (see above), and configure email alerts (Section 2) before your first run.


2. Configure email alerts

FALYS sends alert emails through a Gmail account over SMTP. You need a dedicated Gmail account for this -- don't use your personal inbox, since the credential lives in a config file on the server.

  1. Create (or choose) a Gmail account that exists only to send FALYS alerts.
  2. Turn on 2-Step Verification on that account: myaccount.google.com -> Security -> 2-Step Verification.
  3. Generate an App Password: myaccount.google.com -> Security -> App Passwords -> create one named "FALYS". Google gives you a 16-character password -- copy it.
  4. In server/, copy .env.example to .env:
    cd server
    cp .env.example .env        # Windows: copy .env.example .env
  5. Open .env and fill in:
    FALYS_GMAIL_SENDER=your-dedicated-account@gmail.com
    FALYS_GMAIL_APP_PASSWORD=the16characterapppassword
  6. Save. server.py loads .env automatically -- no extra steps.

Keep .env private. Don't commit it to git, don't upload it anywhere, don't include it if you ever share this project folder with anyone. If an app password is ever exposed, revoke it immediately at myaccount.google.com -> Security -> App Passwords and generate a new one -- old alerts will simply start failing until you update .env with the replacement.

If you'd rather set these as real environment variables instead of a .env file (e.g. for a system service), see run_falys.sh.example / run_falys.ps1.example in server/ for the equivalent launcher scripts -- copy one, fill it in, and use it in place of running server.py directly. Don't fill in both .env and a launcher script with different values; whichever sets a variable first wins.

Until email is configured, the server still runs fine -- alerts are just logged as skipped instead of sent.


3. First run and login

Start the server (whichever option you chose in Section 1). On first run, watch the console output:

========================================================================
[FALYS][FIRST RUN] The following secret(s) were not set via the
environment, so a new random value was generated and saved to
the PostgreSQL database (table server_kv) for reuse on future restarts:
    FALYS_ADMIN_PASSWORD  (admin console password):  <random value>
    FALYS_DISCOVERY_TOKEN  (LAN discovery token):  <random value>
Set these explicitly via your environment for any real deployment
instead of relying on the auto-generated value.
[FALYS] Log in to the dashboard with username 'admin' and
the auto-generated password printed above.
========================================================================

Copy that generated password (or set FALYS_ADMIN_PASSWORD yourself in .env beforehand to skip this). Open the dashboard URL shown in the console and log in with:

The generated password is saved in the PostgreSQL database so it survives a restart -- but set FALYS_ADMIN_PASSWORD explicitly in .env for any real deployment rather than relying on that.


4. TLS (HTTPS)

TLS is on by default. The server generates its own self-signed certificate on first run under server/certs/. This is expected -- there's no public certificate authority for a LAN-only server, so your browser will show a certificate warning the first time you visit the dashboard; that's normal for a self-signed cert and safe to proceed through on your own network.

Each deployed agent pins the server's exact certificate (shipped as cert.pem inside its deployment package) rather than relying on a browser-style trust chain. If you ever regenerate the server's certificate (e.g. its LAN IP changes enough that the old certificate no longer covers it), redeploy affected agents so they pick up the new cert.pem.

If your organization has a real internal CA-issued certificate, point FALYS_TLS_CERT / FALYS_TLS_KEY at it in .env instead of using the self-signed default. To disable TLS entirely for a throwaway local test (never for a real deployment), set FALYS_TLS_ENABLED=0.


5. Deploy your first agent

From the dashboard, go to Agents -> Deploy a New Agent:

  1. Choose the platform (Windows or Linux).
  2. Enter the folder on the target machine you want watched (e.g. C:\Monitor or /var/data).
  3. Enter the alert email for this agent -- the address that should receive alerts for events from this specific endpoint.
  4. Click Generate & Download.

This produces a zip containing the compiled agent binary, a config.json pre-filled for this deployment, the server's cert.pem and a short README.txt. Copy the extracted folder to the target machine and:

That's the whole install -- no Python, no manual service setup on the endpoint either way.

The compiled agent binaries are already in server/build_output/, so the button works instantly for both platforms. Rebuild an agent binary whenever you modify the agent source: run agent/build_exe.ps1 (Windows) or agent/build_exe_linux.sh (Linux) and drop the result, with its VERSION.txt, into server/build_output/. Full details are in Agent Deployment.

For verified (not just process-owner) attribution of who touched a file, see File Event Attribution -- the compiled agents set this up for you during install (see that section for when a manual step is still needed).

Always deploy agents through the dashboard's Agents page -- it's the only supported way to get a correctly-configured config.json (server URL, certificate, and a unique per-agent key) onto an endpoint.

6. Environment variable reference

All variables below can be set in server/.env (copy from .env.example) or as real environment variables. See .env.example for the full commented version.

Variable Required? Default Purpose
FALYS_DATABASE_URL Yes (none) PostgreSQL connection URL, e.g. postgresql://falys:pw@localhost:5432/falys
FALYS_DB_HOST / _PORT / _NAME / _USER / _PASSWORD / _SSLMODE Alternative to the URL localhost / 5432 / falys / falys / (empty) / prefer Individual connection settings (ignored when FALYS_DATABASE_URL is set)
FALYS_DB_POOL_MAX No 20 Max simultaneous database connections
FALYS_DB_CONNECT_TIMEOUT_SECONDS No 30 How long (seconds) the server keeps retrying the database connection at startup
FALYS_EMAIL_WORKERS No 6 Background threads that send alert e-mails
FALYS_EMAIL_QUEUE_MAX No 5000 Alert e-mails that can wait in the queue; beyond this they are not sent and are recorded in the e-mail log
FALYS_WAITRESS_THREADS No 128 Worker threads of the built-in web server (dashboard connections and agent requests)
FALYS_RECENT_CACHE_SIZE No 5000 Recent events kept in memory for the live dashboard
FALYS_NO_AUTO_OPEN No (off) Set to 1 to stop the server opening the dashboard in a browser at startup (use on headless servers and services)
FALYS_TRUST_PROXY No (off) Set to 1 only when FALYS runs behind a reverse proxy you control that sets X-Forwarded-For
FALYS_FORCE_HTTPS No (off) Set to 1 to keep the session cookie Secure when TLS is terminated by a proxy in front of FALYS (FALYS_TLS_ENABLED=0)
FALYS_GMAIL_SENDER For email alerts (none) Gmail address alerts are sent from
FALYS_GMAIL_APP_PASSWORD For email alerts (none) App password for that Gmail account
FALYS_ALERT_RECIPIENTS No (none) Comma-separated fallback recipients
FALYS_SENDER_NAME No FALYS Display name on alert emails
FALYS_ADMIN_USERNAME No admin Dashboard login username
FALYS_ADMIN_PASSWORD Recommended auto-generated Dashboard login password
FALYS_SECRET_KEY Recommended random per-restart Session signing key
FALYS_DISCOVERY_TOKEN No auto-generated Shared token for LAN auto-discovery
FALYS_HTTP_PORT No 8088 Port the dashboard/API listens on
FALYS_TLS_ENABLED No 1 Set to 0 to disable HTTPS (local test only)
FALYS_TLS_CERT / FALYS_TLS_KEY No self-signed, auto-generated in certs/ Use your own certificate instead

7. Troubleshooting

Using the Dashboard

After you log in (see First run and login in Installation & Setup) you land on the Home page. The menu on the left takes you to every other page. The screenshots below use example data from two test machines.

Home

Home page with summary cards

The Home page shows four summary cards: endpoints protected (and how many are online right now), total events logged, threats detected, and email alerts sent. Click Open Dashboard to see the live view.

Dashboard

Dashboard with live event timeline

The Dashboard updates in real time. The tiles at the top show endpoints protected, events today, threats detected, alerts sent, agents online and events per minute. Below them, the Live Event Timeline lists the newest file events as they arrive.

Dashboard charts

Scroll down for the 24-hour event trend, the event distribution (created, modified, deleted, moved), events per host, and the severity breakdown. The Threat Detections panel summarises how many events fall into each risk class and lists the most recent high-priority events and any active incidents.

Dashboard status panels

At the bottom you will find the most frequently modified files, live agent status, recent alerts and a small system-health summary.

Event History

Event history with alert email log

Click Emails to open the alert-email log. Each entry shows the recipient and whether the email was sent or throttled. Throttling limits alerts to one per recipient every 30 seconds so a burst of activity does not flood an inbox; throttled events are still logged and still visible in the dashboard.

Event history table

All Events lists every event the agents have reported. Use Export CSV or Export JSON to download the data. The User column shows who touched the file; it is marked UNVERIFIED until you enable OS-level auditing on the endpoint (see File Event Attribution).

Timeline

Activity timeline

The Timeline shows the same events in chronological order, colour-coded by type (created, modified, moved, deleted).

Agents

Agents page

This is where you deploy and monitor agents.

  1. Choose the Platform (Windows or Linux).
  2. Enter the folder to watch on the target machine.
  3. Enter the alert email that should receive alerts for that agent.
  4. Click Generate & Download, copy the downloaded package to the endpoint, and run it as described in Agent Deployment.

Every connected agent is listed below the form with its event count, IP address, online/offline status and the time it was last seen.

Detections

Detections overview

The Detections page shows how the detection engine classified every event as Critical, High Risk, Suspicious, Low Risk or Legitimate. Click Show guide under What do these labels mean? for a plain-language explanation. Related events that the engine has linked together (for example a burst of changes on one host) appear as Correlated Incidents.

Classified events

Classified Events lists each event with its classification, score, confidence and the reasons behind the score. You can search by host or file and filter by classification. See Detection Engine for how scores are calculated.

Settings

Settings page

Setting What it does
Real-time File Monitoring Master switch: when off, agent events no longer trigger alerts.
Email Alerting (Gmail) Master switch for alert emails. The line under it reads Not configured until a Gmail sender and app password are set in .env.
High Severity Threshold How many high-severity events on one host trigger an alert: Immediate, After 1 event or After 3 events.
Auto Agent Discovery Keeps polling agent activity automatically.
Agent Offline Threshold Marks an agent Offline after no events for 2 minutes, 5 minutes, 15 minutes or 1 hour.
Real-time Refresh Interval How often the Dashboard and Agents pages poll for updates (2, 3, 5 or 10 seconds).

Changes are saved immediately.

Customization

This chapter explains what you can change and how, from no-code settings to editing the source. Most changes need no restart.

1. Settings page (no code)

Alerting, thresholds, the offline timeout and the refresh interval are all on the Settings page (see Using the Dashboard).

2. Configuration file (.env)

Ports, the admin login, e-mail credentials, TLS and the database are set in FALYS-server/server/.env. Every variable is listed in the Environment variable reference in Installation & Setup. Restart the server after editing.

3. Detection rules (no restart)

The rules that decide how risky an event is are stored in the database and are edited through the dashboard's API. Changes apply within a couple of seconds.

How to make a change. Log in, open the Settings page, open your browser's developer console (F12 -> Console) and paste this helper once:

const csrf = document.querySelector('meta[name="csrf-token"]').content;
async function api(path, body) {
  const r = await fetch(path, {
    method: body ? 'POST' : 'GET',
    headers: {'Content-Type': 'application/json', 'X-CSRFToken': csrf},
    body: body ? JSON.stringify(body) : undefined
  });
  return r.json();
}

Then read the current rules with await api('/api/risk-rules').

Rule Meaning Default
default_severity_by_event Baseline severity per event type created/modified = low, deleted/moved = medium
sensitive_path_rules List of {pattern, severity}; patterns are case-insensitive wildcards matched against the full path built-in list (e.g. *shadow, *.pem, *id_rsa*, cron and log paths)
permission_change_severity Severity when a file's permissions change high
frequency_rule If one host exceeds event_threshold events in window_seconds, escalate to escalate_to 50 events / 60 s -> critical
alert_threshold Minimum severity that sends an alert e-mail high
alert_on_new_correlated_incidents E-mail once when a new incident opens true
critical_hosts Host names (wildcards allowed) treated as high-value assets empty

Severity values are low, medium, high and critical. Rules can only raise attention: the highest severity that any rule assigns always wins.

Example - treat a folder as critical. Saving a list replaces the whole list, so read it first, add to it, and save it back:

const rules = await api('/api/risk-rules');
rules.sensitive_path_rules.push({pattern: '*/finance/*', severity: 'critical'});
await api('/api/risk-rules', {sensitive_path_rules: rules.sensitive_path_rules});

Example - e-mail on Suspicious events too, and mark servers as critical:

await api('/api/risk-rules', {alert_threshold: 'medium', critical_hosts: ['DC-*', 'FINANCE-01']});

Example - change the burst rule (send the whole object):

await api('/api/risk-rules', {frequency_rule: {window_seconds: 30, event_threshold: 100, escalate_to: 'high'}});

4. Detection engine tuning (no restart)

Scoring weights and class boundaries are read from /api/engine-config. You can send just the values you want to change; nested values are merged.

Section Keys Defaults
weights rule, anomaly, correlation (should add up to about 1.0) 0.5 / 0.3 / 0.2
classification_thresholds minimum score for low_risk, suspicious, high_risk, critical 10 / 30 / 55 / 80
anomaly rate_zscore_threshold, business_start_hour, business_end_hour 3.0 / 7 / 20
correlation window_seconds, burst_event_threshold, burst_min_score 120 / 5 / 30
// Working hours 09:00-18:00 and a stricter "High Risk" boundary
await api('/api/engine-config', {anomaly: {business_start_hour: 9, business_end_hour: 18},
                                 classification_thresholds: {high_risk: 50}});

Reset to defaults: run DELETE FROM server_kv WHERE key = 'risk_rules'; (or 'engine_config', 'settings') in PostgreSQL.

5. What each agent watches (config.json)

Each downloaded agent package contains a config.json. You can edit it on the endpoint and restart the agent service (FalysAgent on Windows, sudo systemctl restart falys-agent on Linux).

Key Meaning Default
watch_dir Folder to monitor set when you generate the package
alert_email Recipient for this agent's alerts set when you generate the package
extra_exclude_patterns Extra wildcard patterns to ignore, e.g. ["*/backups/*", "*.tmp"] none
debounce_seconds Merge rapid repeat events on the same file 2
max_hash_bytes Largest file that is hashed 200 MB
worker_threads Parallel event workers 4 (8 when the watched folder is a whole drive such as C:\ or /)

Do not change server, api_key or discovery_token; they tie the agent to your server. Temporary folders, caches, node_modules and .git are already excluded by default.

6. E-mail alerts

In server/email_service.py:

Per-agent recipients are set when you generate a deployment package; FALYS_ALERT_RECIPIENTS in .env is the server-wide fallback list.

7. Branding and look

8. Changing the source code

9. Retention

Events are kept until you delete them. See Retention in PostgreSQL Setup for the pruning command.

PostgreSQL Setup

FALYS keeps all of its data in PostgreSQL. There are no event files, JSON settings files or key files anywhere. The only thing the server still writes to disk is its TLS certificate and key (server/certs/, or the files you point FALYS_TLS_CERT / FALYS_TLS_KEY at) -- the TLS libraries need real file paths, and many teams supply their own CA-issued certificate anyway.

What is stored where

Table Holds
server_events Every file event, with its classification, risk score, reasons and correlation info (the full record is in the data JSONB column; host, event_type, severity, classification, event_time are indexed columns). The table's id is the event's _id.
server_agent_keys One row per issued agent API key -- SHA-256 hash only, never the key itself.
server_kv Small admin-editable documents: settings, risk_rules, engine_config, tunnel_trust, generated_secrets.
server_email_logs One row per alert email attempt (sent / skipped / failed).

server/schema.sql is the exact DDL. The server applies it automatically (CREATE TABLE IF NOT EXISTS) on start, so you normally never run it yourself -- it is there for teams that prefer to apply schema with their own tooling or a privileged account.

All tables are prefixed server_, so the same database can safely be shared with other applications.

Setting it up

Docker (quickest):

cd server
docker compose -f docker-compose.postgres.yml up -d

Existing PostgreSQL server (PostgreSQL 12+, tested on 16) -- as a superuser:

CREATE ROLE falys LOGIN PASSWORD 'choose-a-strong-password';
CREATE DATABASE falys OWNER falys;

On PostgreSQL 15+ the owner of a database can create tables in it. If you use a different setup, make sure the role has CREATE on the schema it will use:

\c falys
GRANT USAGE, CREATE ON SCHEMA public TO falys;

Then set the connection in server/.env:

FALYS_DATABASE_URL=postgresql://falys:choose-a-strong-password@db-host:5432/falys?sslmode=require

or the separate FALYS_DB_HOST, FALYS_DB_PORT, FALYS_DB_NAME, FALYS_DB_USER, FALYS_DB_PASSWORD, FALYS_DB_SSLMODE variables. Use sslmode=require (or stricter) whenever the database is on another machine.

Behaviour to know about

Backups

Everything is in the one database, so a standard PostgreSQL backup is a complete FALYS backup:

pg_dump -Fc -h db-host -U falys falys > falys-$(date +%F).dump
# restore into an empty database:
pg_restore -h db-host -U falys -d falys --clean --if-exists falys-2026-09-24.dump

Retention

Events are kept forever unless you prune them. To drop events older than, say, 400 days (the dashboard's live counters are rebuilt from the table at the next server restart):

DELETE FROM server_events WHERE event_time < now() - interval '400 days';

Upgrading from a flat-file install

Earlier FALYS versions wrote a server/logs/ folder (events/*.jsonl, agent_keys.json, settings.json, ...). To move that data across:

  1. Stop the FALYS server.

  2. Set up PostgreSQL and server/.env as above.

  3. From server/, run:

    python migrate_flat_files_to_postgres.py            # reads ./logs
    # python migrate_flat_files_to_postgres.py --logs-dir /path/to/old/logs

    Event ids are preserved, so agents and integrations that track event ids carry on seamlessly. The script never modifies or deletes the old files, and is safe to re-run. Existing documents in the database are kept unless you pass --overwrite.

  4. Start the server, check the dashboard, then archive the old logs/ folder.

Running the tests

createdb falys_test        # a throwaway database -- its FALYS tables are emptied
cd server
FALYS_TEST_DATABASE_URL=postgresql://falys:pw@localhost:5432/falys_test \
  python -m unittest discover -s tests -p "test_postgres_store.py" -v

Agent Deployment

Endpoints on both Windows and Linux receive a single compiled binary and don't need Python installed -- the same "just run the file" model as most compiled agents.

No PowerShell, no batch files, no pip install, no manual python agent.py on either platform.

The monitoring logic itself (agent.py, attribution.py, discovery.py) is identical on both platforms -- it's compiled into each binary as-is. Communication with the server and the registration model are the same regardless of platform.

Architecture

agent/agent.py                <- monitoring logic (main()/stop())

agent/windows_service.py      <- pywin32 Windows Service wrapper +
                                  self-install logic. Compiled into
                                  FalysAgent.exe.
agent/FalysAgent.spec         <- PyInstaller build spec (Windows)
agent/build_exe.ps1           <- one-time build script (Windows, IT-run)

agent/linux_service.py        <- systemd wrapper + self-install logic.
                                  Compiled into falys-agent.
agent/FalysAgentLinux.spec    <- PyInstaller build spec (Linux)
agent/build_exe_linux.sh      <- one-time build script (Linux, IT-run)

server/deployment_builder.py  <- packages the prebuilt binary (either
                                  platform) + a fresh per-deployment
                                  config.json
server/build_output/          <- the compiled FalysAgent.exe and
                                  falys-agent binaries go here (build
                                  them once -- see below)

Building the agent binaries

Ready-made agent binaries -- FalysAgent.exe (Windows, x86-64) and falys-agent (Linux, x86-64) -- are included in server/build_output/, together with a VERSION.txt (2.2.0) that the dashboard checks. The dashboard's Agents page hands them out directly.

They were built from the agent source in agent/ using the included PyInstaller build scripts (agent/build_exe.ps1 and agent/build_exe_linux.sh). SHA-256 checksums of the shipped binaries:

File SHA-256
FalysAgent.exe ad611624380ea1e47458203461d42a0b821fc632a21268a7648739981e613817
falys-agent 871bd88fa7f45536c7d66cd575dc3457c17d52bb9a471242856123e3fb51ffaf

If you would rather not use the shipped binaries, delete them from server/build_output/ and build your own as described below; until a binary for a platform exists, the Agents page gives you a build kit for that platform (source + instructions) instead.

To build (and again whenever you modify the agent source):

Linux-specific note: unlike a statically-linked Go/Rust binary, a PyInstaller binary is only portable across machines with a compatible-or-newer glibc than the one it was built on. Build falys-agent on the oldest distro version you need to support (see build_exe_linux.sh's header comment) so that endpoints on older distros don't hit a GLIBC_2.XX not found error. Windows has no equivalent concern.

Why a self-installing EXE instead of an MSI

A self-installing exe gets you the same practical end-user experience as an .msi -- double-click, one admin prompt, done -- without requiring the WiX Toolset to build and maintain an installer.

If your organization specifically needs .msi for a software-push tool (SCCM/Intune/GPO), the cleanest path is to wrap the existing FalysAgent.exe in a thin WiX or Inno Setup installer whose only job is to lay the exe + config.json on disk and run FalysAgent.exe once.

TLS (cert.pem)

The server terminates HTTPS itself using a self-signed certificate it generates on first run under server/certs/. There's no public CA for a LAN-only server, so instead of a browser-style certificate chain, each agent pins the server's exact certificate: it ships as cert.pem inside the deployment package, next to config.json, and agent.py verifies every connection against that specific file (see agent.py's CERT_PATH/_verify_param()).

Practical implications:

Windows service startup

The compiled Windows agent is started by the Service Control Manager (SCM) on boot, exactly like any other Windows service: SvcDoRun reports SERVICE_RUNNING immediately, then does its actual work (loading config, connecting to the server) on a background thread with retry-with-backoff and logging (service.log next to the exe, plus the Windows Event Log). That keeps the service in a running/retrying state rather than exiting if a config or network issue is hit right at startup. A zero-argument launch is routed to StartServiceCtrlDispatcher() (the SCM's own launch signature), falling back to the interactive double-click installer flow only when that call fails -- the one reliable signal that the process wasn't started by the SCM.

Linux has no equivalent handshake to get right: systemd's --service-run path is a straightforward foreground run with its own retry-with-backoff loop (see linux_service.py's module docstring for the full comparison).

File Event Attribution

What this is

By default, every FALYS agent reports the OS user its own process is running as (getpass.getuser()). That is not the same thing as "the user who touched the file" — a service, a scheduled task, or an agent running under a shared/service account can all touch files on behalf of someone else, and the process-level user won't reflect that.

To get a trustworthy answer to "who actually did this", FALYS queries the operating system's own audit trail:

Platform Mechanism Requires
Linux auditd / ausearch An audit watch rule on the monitored folder
Windows Security event log (wevtutil, event ID 4663) Object Access auditing + a SACL on the monitored folder

Neither is enabled by default on a fresh OS install. The compiled agent binaries apply this audit setup automatically while they install themselves (Windows: the one admin prompt; Linux: sudo), provided the watched folder already exists on the endpoint at that moment. If the folder did not exist yet, or you later change watch_dir, create the folder and re-run the agent installer, or run the setup script below for that folder. Until auditing is in place for the watched folder, attribution lookups will consistently return unverified — this is expected, not a bug, and the dashboard labels it as such rather than guessing.

How to tell if it's working

Every event the agent sends includes:

{
  "user": "alice",                  // process-level user (always present)
  "attributed_user": "jdoe",           // OS-audit-verified user, or null
  "attribution_verified": true,        // true only if a real audit record was found
  "attribution_source": "auditd"       // or "security_log", or a reason code if unverified
}

In the Event History page, the User column shows a green "Verified" badge with the audited account when attribution_verified is true, and an amber "Unverified" badge with the process-level fallback otherwise.

attribution_source when unverified tells you why, e.g.:

Linux setup

sudo ./agent/setup_audit_linux.sh /path/to/watched/dir

This installs auditd if needed, adds a persistent watch rule (-w <dir> -p wa -k falys_watch), and loads it immediately.

Verify manually:

sudo auditctl -l | grep falys_watch
touch /path/to/watched/dir/test.txt
sudo ausearch -f /path/to/watched/dir/test.txt -ts recent

Windows setup

Run from an elevated PowerShell prompt:

.\agent\setup_audit_windows.ps1 -WatchDir "C:\watch_falys\monitor"

This enables Object Access → File System auditing (auditpol) and applies a SACL to the watched folder so access to files inside it actually gets logged (auditing policy alone does nothing without a SACL on the object).

Verify manually:

auditpol /get /subcategory:"File System"
Get-Acl -Path "C:\watch_falys\monitor" -Audit | Format-List
New-Item -Path "C:\watch_falys\monitor\test.txt" -ItemType File
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4663} -MaxEvents 5

Technical notes

Detection Engine

What this is

FALYS's /log ingestion route used to do one thing: run a single deterministic rule pass (risk_classifier.py) and store a low/medium/high/critical severity. That still exists, but it's now one of three signals feeding a proper detection engine, modeled on the same pipeline shape commercial EDR/XDR products use (Defender for Endpoint, CrowdStrike Falcon, SentinelOne, Elastic Security): collect context, score from multiple independent angles, correlate across events, and classify with an explicit confidence value plus explainable reasons — not just a bare severity string.

Nothing about the agent API changed. Agents still POST the exact same JSON payload to /log with the same X-API-Key header they always did. All existing routes, the dashboard pages, alerts, and agent deployment/generation flows are unmodified. The engine is entirely a server-side addition.

Architecture

server/
  engine/
    __init__.py       <- public entry point: analyze_event()
    engine.py          <- orchestrator, wires the stages below together
    context.py          <- 1. Context Collection
    rule_scoring.py     <- 2. Rule-Based Risk Scoring
    anomaly.py           <- 3. Behavioral Anomaly Detection
    correlation.py        <- 4. Event Correlation
    classifier.py          <- 5. Confidence-Based Classification
    state.py         <- shared bounded/thread-safe primitives used by
                         every stage above (LRU caches, rolling windows,
                         online mean/variance)
  engine_config_store.py  <- admin-editable weights/thresholds/tuning
                              (PostgreSQL, server_kv "engine_config")
  risk_rules_store.py     <- unchanged: admin-editable rules the RULE
                              stage consumes (sensitive paths, event-type
                              severities, critical_hosts, ...)
  server.py                <- /log calls engine.analyze_event() once per
                               event; everything else (routes, alerts,
                               dashboard) is unchanged

The five stages, per event

  1. Context Collection (context.py) — is this host/user new, is it business hours, is this the first time this user has touched this directory, is the host tagged as a critical asset. Pure fact gathering, no scoring.
  2. Rule-Based Risk Scoring (rule_scoring.py) — the original deterministic logic (event-type baseline, sensitive-path globs, permission-change detection, burst-frequency), now producing a 0-100 score instead of just a tier, admin-editable via /api/risk-rules exactly as before.
  3. Behavioral Anomaly Detection (anomaly.py) — no fixed rules; builds a per-host statistical baseline (online mean/variance of events-per-minute, an hour-of-day activity histogram) and flags deviations from that specific host's own history, plus novelty flags for "first time this user has used this host/path."
  4. Event Correlation (correlation.py) — groups related events within a sliding time window per host: bursts, staged tampering patterns (permission loosened → sensitive file touched → deleted), and cross-host activity by the same attributed user. Produces INC-xxxxxxxxxx incident records, listed at GET /api/incidents.
  5. Confidence-Based Classification (classifier.py) — combines the three scores (weighted, admin-tunable) into one 0-100 score, maps it to a tier (Legitimate / Low Risk / Suspicious / High Risk / Critical), and computes a separate 0-100 confidence value driven by how mature the anomaly baseline is and how many independent signals agree.

Fault tolerance

engine.py's orchestrator wraps every stage individually. If any one stage raises (bad data, an edge case in a statistical calculation, whatever), that stage's contribution degrades to a safe default and a warning is logged — ingestion never stops because a detector had a bug. This mirrors how a production EDR pipeline should behave: a scoring regression should degrade detection quality, not availability.

What gets stored per event

server.py's /log route now stores, in addition to the pre-existing fields:

{
  "severity": "high",                  // legacy, unchanged meaning
  "classification": "High Risk",
  "confidence": 82,
  "risk_score": 71,
  "reasons": [
    "Path matches sensitive-path rule '*shadow' (critical)",
    "File permissions changed from their last known value (escalates to 'high')",
    "Staged tampering pattern on this host: permission_change -> sensitive_hit within 120s"
  ],
  "correlation_id": "INC-11b2ad78e6",
  "signals": {"rule": 65, "anomaly": 35, "correlation": 55}
}

should_alert() (rules.py) decides whether an event sends an alert e-mail. It compares the legacy severity field with the admin-configured alert_threshold, and — when alert_on_new_correlated_incidents is true — also alerts once for the event that opens a new correlated incident. It never alerts while Real-time File Monitoring is switched off in Settings.

New API routes

Route Method Purpose
/api/engine-config GET Read the engine's weights/thresholds/tuning
/api/engine-config POST Patch weights/thresholds (same unknown-key-rejection pattern as /api/risk-rules)
/api/incidents GET Recent correlated incidents (?limit=N)

/api/stats gained a classification_counts field (5-tier breakdown) alongside the existing severity_counts (4-tier) — both are populated from every event going forward; older events default to Legitimate/low rather than skewing the new breakdown.

A new dashboard page, Detections (/detections), shows the classified event table (with expandable reasons and a confidence bar) and the correlated-incidents feed.

Tuning

Everything scoring-related is data, not code, editable at runtime:

Both follow the same pattern: a DEFAULT_* dict, a JSON document in PostgreSQL (table server_kv), and a set_*() that only accepts known keys and merges rather than overwrites nested sub-dicts.

Dynamic IP Discovery

The problem

Both the central server and the agent endpoints are on DHCP, so their IPs change over time. Previously, the agent's config.json had the server's IP hardcoded — once that IP changed, the agent had no way to find the server again, and connectivity was lost completely until someone manually updated config.json with the new address.

The solution

Since the server and all agents share the same enterprise LAN (same broadcast domain/subnet), this uses a small UDP broadcast discovery protocol — no extra infrastructure (no DNS server, no dependency on internet access) required.

What happens on server.py startup

  1. Resolves its own current LAN IP (discovery.get_local_ip()).
  2. Immediately broadcasts a falys_announce UDP packet on the LAN so any agent already running picks up the current IP right away.
  3. Starts a background listener that answers any agent's falys_discover broadcast directly with its current IP.
  4. Starts a background watchdog that re-checks its own IP every 30s; if it ever changes while running, it re-broadcasts immediately.

What happens on agent.py startup

  1. Resolves its own current LAN IP the same way (used in every event's ip field, instead of the old socket.gethostbyname(socket.gethostname()) call, which is unreliable — commonly returns 127.0.1.1 or the wrong interface on multi-homed machines).
  2. Checks whether the server address currently in config.json is still reachable and genuinely the FALYS server (via a /health check — see "Authentication" below). If yes, nothing changes.
  3. If not, broadcasts a falys_discover request on the LAN and waits for an authenticated reply. If found, updates config.json immediately so the next restart also starts from the right address.
  4. If nothing answers, the agent keeps queuing events locally (existing offline-queue behavior) and retries discovery every 20s in the background until the server is found.
  5. For the rest of the session, a background listener watches for the server announcing itself again (step 2 of the server's startup, or its periodic re-announce if its IP changes later) and updates immediately — so a live agent recovers from a server IP change without needing to be restarted.

Authentication ("is this really the server?")

Broadcast discovery on its own just tells you something answered on a port — not that it's genuinely the FALYS server and not some other device. Both sides are configured with the same shared secret (FALYS_DISCOVERY_TOKEN on the server, and discovery_token in every agent's config.json). Every discovery message carries a one-way hash of that secret (sha256("FALYS_DISCOVERY_V1:" + token + ":" + time-bucket), where the time bucket changes every 5 minutes so a captured packet cannot be replayed indefinitely) — never the secret itself — so:

There is no shared default token. If FALYS_DISCOVERY_TOKEN is not set in .env, the server generates a random token on first start and keeps it in PostgreSQL (see First run and login). Every agent package generated from the dashboard gets the server's current token written into its config.json automatically. To use a value of your own, set FALYS_DISCOVERY_TOKEN in .env before generating agent packages. If the token is changed later, agents built with the old one keep working while the server's address stays the same, but they can no longer re-find the server automatically until they are redeployed.

Verified behavior (tested in this environment)

Network requirements and behavior notes

Credits & Third-Party Licenses

FALYS is sold under the standard Envato Market license you selected when you purchased it (see https://codecanyon.net/licenses/standard). It bundles or depends on the following third-party open-source components, each of which remains under its own license. The same list is in Licenses.txt.

Component Version License Used for
Flask 3.1.1 BSD-3-Clause Web framework (dashboard + API)
Werkzeug, Jinja2, MarkupSafe, ItsDangerous, Click, Blinker see requirements.lock BSD-3-Clause Flask dependencies
Flask-WTF / WTForms 1.2.2 / 3.2.1 BSD-3-Clause Form handling / CSRF protection
waitress 3.0.2 ZPL-2.1 Production web server
cheroot 10.0.1 BSD-3-Clause HTTPS web server
requests (+ urllib3, idna, charset-normalizer) 2.32.3 Apache-2.0 / MIT / BSD HTTP client
certifi 2025.1.31 MPL-2.0 CA bundle
cryptography (+ cffi, pycparser) 44.0.0 Apache-2.0 OR BSD-3-Clause TLS certificates
psycopg2-binary 2.9.10 LGPL-3.0-or-later (with OpenSSL exception) PostgreSQL driver
watchdog 4.0.2 Apache-2.0 File-system monitoring (agent)
pywin32 312 PSF-2.0 Windows service support (agent, Windows only)
PyInstaller build tool GPL-2.0-or-later with bootloader exception Used to compile the agent/server binaries
Font Awesome Free 6.5.1 Icons CC BY 4.0, fonts SIL OFL 1.1, code MIT Dashboard icons (loaded from cdnjs)
Chart.js latest (CDN) MIT Dashboard charts (loaded from jsDelivr)

Python itself is used under the PSF License. The dashboard pages load Font Awesome and Chart.js from public CDNs (cdnjs.cloudflare.com and cdn.jsdelivr.net); those files are not bundled in the download.