Anti-Ransomware Realtime Scanner (inotify + YARA + Delayed Scan)

1. Overview

The Anti-Ransomware realtime daemon implements BastionGuard’s filesystem-monitoring and rapid-response service for suspicious file activity. It combines inotify-based realtime monitoring, YARA scanning, delayed verification, archive inspection, and local authenticated alerting to detect ransomware-like behaviors during file creation, modification, rename, and late-stage write completion.

The service is designed for continuous execution on Linux desktop systems, with explicit support for cross-distro runtime behavior, safe recursive monitoring, best-effort process attribution, structured logging, and integration with local BastionGuard UI or companion components through a localhost TCP channel protected by a shared token.

The daemon integrates with:

  • inotify – realtime recursive filesystem event monitoring
  • YARA – signature/rule-based detection on files and suspected writer executables
  • ArchiveInspector – sandboxed archive inspection for suspicious archive outputs
  • DelayedScanQueue – deferred secondary verification after a fixed delay
  • Local TCP alerting – authenticated localhost notifications on port 1025
  • loginctl – active desktop-session user detection
  • glib i18n – translated runtime logging via _()

2. Global Runtime Controls and Logging

2.1 Lifecycle Flag and Signal Handling

The daemon uses a global atomic lifecycle flag:

static std::atomic<bool> running(true);

Termination is handled through standard process signals:

  • SIGINTsignal_handler() sets running = false
  • SIGTERMsignal_handler() sets running = false

The shutdown flow joins the realtime and delayed worker threads, then calls clean_shutdown(), which is currently a placeholder hook for future cleanup logic.

2.2 Log File Management

Operational logs are appended to:

/var/log/BastionGuard/antiransom_inotify.log

The helper ensure_log_dir() guarantees:

  • Creation of /var/log/BastionGuard if missing
  • Directory permissions 0755
  • Creation of the log file if missing
  • Log file permissions 0644

Messages are written through log_msg(), which prefixes each entry with a local-time timestamp in the format:

[YYYY-MM-DD HH:MM:SS] message

2.3 PID Attribution Rate Limiting

The current implementation introduces a best-effort PID attribution rate limiter using:

  • g_pid_rl_mtx – mutex protecting attribution timing state
  • g_pid_last_ms – last attribution attempt timestamp per path

The helper should_guess_pid(path, min_interval_ms) prevents repeated PID lookups on the same path within a short interval. The current usage applies a default interval of about 600 ms for writer-related events.


3. Desktop User Detection

3.1 Purpose

The daemon targets an active desktop-user context in order to:

  • Resolve the correct home directory to monitor
  • Copy the shared ransomware token into the user runtime area
  • Build a desktop-relevant watch list

3.2 Detection Strategy

The function detect_desktop_user() identifies the active graphical user via loginctl. The detection flow is:

  1. Enumerate users through:
    loginctl list-users --no-legend | awk '{print $2}'

  2. For each candidate user, query:
    loginctl show-user <user> --property=State --property=Display

  3. Select the first user where:
    • State=active
    • Display is not empty

When a valid graphical user is found, the daemon logs the selected identity and uses it as the desktop security context.


4. Security Token Management and Local Alerting

4.1 Token Purpose and Paths

A shared security token is used to authenticate local alerts sent over TCP to a localhost listener. The token is stored at two levels:

  • Root token:
    /etc/BastionGuard/ransomware.token

  • User copy:
    ~/.local/share/BastionGuard/ransomware.token

This allows the realtime scanner and UI-side components to share a trust secret for local event delivery.

4.2 Load-or-Create Logic

The function load_or_create_token() implements the following logic:

  1. If /etc/BastionGuard/ransomware.token exists:
    • Read the first line into SECURITY_TOKEN
    • Log successful load
    • Copy the token to the active desktop user
  2. If the file is missing:
    • Create /etc/BastionGuard if needed
    • Read 32 random bytes from /dev/urandom
    • Encode them into a 44-character Base64-like token
    • Write the token to /etc/BastionGuard/ransomware.token
    • Apply permissions 0640 and ownership root:root
    • Copy the token to the active desktop user

4.3 Token Copy to User Profile

The helper copy_token_to_user():

  • Detects the active desktop user
  • Resolves the user home directory via getpwnam()
  • Creates:
    ~/.local/share/BastionGuard

  • Writes:
    ~/.local/share/BastionGuard/ransomware.token

  • Applies user ownership and permissions 0644

4.4 Local Socket Alert Protocol

Realtime detections are reported over a localhost TCP socket:

  • Destination: 127.0.0.1:1025
  • Transport: AF_INET / SOCK_STREAM

The generic YARA alert sender send_socket_alert(file, family) transmits messages in the format:

<TOKEN>|<file_or_exe_path>|<family>

Common family values emitted by the current implementation include:

  • YARA_PROCESS_MATCH
  • YARA_FILE_MATCH

Archive-related detections are sent through a dedicated helper, send_archive_alert(), which formats the family field as:

ARCHIVE_<reason_code>

5. Process Attribution and File Classification

5.1 Best-Effort Writer PID Detection

The daemon attempts to attribute a file event to a process through guess_writer_pid(path). The function scans:

  • /proc/<pid>/fd for all numeric PIDs
  • Each file-descriptor symlink via readlink()

If any open descriptor resolves exactly to the affected file path, the corresponding PID is returned.

This is a best-effort heuristic and is not guaranteed in all timing or race conditions.

5.2 Process Executable Resolution

The helper get_process_exe(pid) resolves:

/proc/<pid>/exe

This executable path is then passed to YARA for process-level scanning.

5.3 Safe Regular-File Check

The helper is_regular_file_safe(path) uses std::filesystem::is_regular_file(...) with an error code guard to avoid exceptions and unsafe path assumptions during archive inspection routing.

5.4 Archive Path Heuristics

The current implementation adds a dedicated archive-path heuristic through looks_like_archive_path(path). It lowercases the path and recognizes these suffixes:

  • .zip
  • .7z
  • .rar
  • .tar
  • .gz
  • .tgz
  • .bz2
  • .xz

This heuristic is used only to decide whether archive inspection should be attempted; it is not a trusted magic-byte format validator.


6. YARA Engine Integration

6.1 Rule Loading

The embedded YaraEngine class encapsulates YARA initialization, compilation, scanning, and teardown.

Rule loading occurs through load_rules(dirpath), which:

  • Calls yr_initialize()
  • Creates a compiler with yr_compiler_create()
  • Iterates the rule directory
  • Loads files ending in .yara
  • Compiles them through yr_compiler_add_file()
  • Finalizes a consolidated YR_RULES object through yr_compiler_get_rules()

The current implementation loads only .yara files, not .yar.

6.2 Scan Callback and Match Logging

The static callback used by YARA sets a match flag when it receives CALLBACK_MSG_RULE_MATCHING and logs the matched rule identifier in the format:

MATCH → <rule_identifier>

6.3 Supported Scan Targets

The engine supports two scan targets:

  • scan_pid(pid) – resolves and scans the executable behind /proc/<pid>/exe
  • scan_file(path) – scans the affected file path directly

Both scan methods use:

yr_rules_scan_file(..., SCAN_FLAGS_FAST_MODE, ...)

When a match is found, the engine sends a local alert through send_socket_alert().

6.4 Thread Safety

The YaraEngine protects its shared YR_RULES state through an internal mutex, ensuring that concurrent scans from the realtime thread and delayed queue do not race the engine state.


7. Immediate and Delayed Scanning Strategy

7.1 Reliable Immediate Scan Helper

The code still contains a stabilization helper, reliable_scan(yara, path), intended to reduce false negatives when events fire before writes are fully committed.

Its behavior is:

  • Up to 20 attempts
  • 50 ms delay between attempts
  • Checks that the file exists and has size greater than zero
  • Applies an additional 20 ms stabilization delay before scanning
  • Falls back to a final scan if the file still exists

In the current code path, realtime event handling calls yara.scan_file(full) directly rather than routing through reliable_scan(), so the helper exists but is not currently used in the main event loop.

7.2 DelayedScanQueue

The DelayedScanQueue performs secondary verification after a fixed delay. It is designed to catch behaviors such as:

  • Multi-stage writes
  • Late file replacement
  • Atomic rename after encrypted output generation
  • Post-write transformations

Each queue item stores:

  • path
  • when – a steady-clock execution timestamp

The default enqueue delay is:

30000 ms

which corresponds to 30 seconds.

7.3 Delayed Worker Loop

The delayed queue runs in its own thread. Every 500 ms, it wakes up, collects expired queue items, and rescans each path through yara.scan_file().

Lifecycle control is handled by:

std::atomic<bool> active{true}

Calling stop() sets active = false, allowing a clean thread shutdown.


8. Archive Inspection Integration

8.1 ArchiveInspector Initialization

The current daemon integrates the archive analysis subsystem through:

ArchiveInspector archive_inspector(
    "/usr/libexec/bastionguard/archive_worker",
    10
);

This means archive analysis is delegated to a sandboxed worker executable with a timeout of 10 seconds.

8.2 Archive Event Routing

The helper maybe_inspect_archive(full) is invoked for writer-related file events. Its flow is:

  1. Check whether the path looks like an archive via looks_like_archive_path()
  2. Ensure the target is a regular file through is_regular_file_safe()
  3. Run:
    archive_inspector.inspect(full)

  4. Log the structured archive result through log_archive_result()
  5. If analysis failed, log the failure and stop
  6. If the worker result does not classify the file as ZIP, stop
  7. If the risk is not CLEAN, emit an authenticated archive alert

8.3 Archive Alert Semantics

Archive alerts are emitted only when the inspection result indicates a non-clean archive risk. The alert family is built as:

ARCHIVE_<reason_code>

The archive result is also logged with fields such as:

  • success
  • is_zip
  • risk
  • score
  • reason
  • detail

This is the main functional addition compared to the earlier daemon design.


9. Inotify Engine

9.1 Initialization

The InotifyEngine owns the realtime monitoring layer. It initializes an inotify instance using:

inotify_init1(IN_NONBLOCK)

Watch descriptors are stored in:

std::map<int, std::string> watch_map

and protected by map_mutex.

9.2 Watch Registration

Directory watches are added with the following broad mask:

IN_CREATE | IN_CLOSE_WRITE | IN_MODIFY |
IN_MOVED_TO | IN_MOVED_FROM | IN_ATTRIB |
IN_DELETE | IN_DELETE_SELF | IN_ISDIR

This covers the most relevant file lifecycle operations for ransomware-style write behavior, overwrite workflows, and post-write metadata changes.

9.3 Recursive Monitoring

The method add_recursive(root) traverses the directory tree with:

std::filesystem::recursive_directory_iterator(
    root,
    std::filesystem::directory_options::skip_permission_denied,
    ec)

This ensures broad cross-distro compatibility while avoiding failures on protected or inaccessible subtrees.

9.4 Event Decoding

For diagnostic transparency, the engine decodes event masks into human-readable labels through decode_mask(). Logged names include:

  • CREATE
  • CLOSE_WRITE
  • MODIFY
  • MOVED_TO
  • MOVED_FROM
  • ATTRIB
  • DELETE
  • DELETE_SELF
  • ISDIR

9.5 New Directory Handling

If an inotify event is flagged with IN_ISDIR, the engine immediately adds a watch for the new directory. This preserves recursive coverage for directories created after startup.

9.6 Relevant File Events

The engine currently treats the following mask group as relevant file events:

IN_CREATE | IN_CLOSE_WRITE | IN_MODIFY |
IN_MOVED_TO | IN_DELETE_SELF | IN_ATTRIB

Events outside this set are ignored for scanning purposes.

9.7 Atomic Rename Detection

An “atomic rename” condition is logged when:

  • IN_MOVED_TO is present
  • IN_CREATE is not present

This targets a common ransomware behavior where encrypted output is written to a temporary file and then moved into place.

9.8 Per-Event Detection Workflow

For writer-related events, currently defined as:

IN_CLOSE_WRITE or IN_MOVED_TO

the engine executes the following workflow:

  1. Throttle PID attribution through should_guess_pid(full, 600)
  2. If allowed, try to resolve the writer PID using guess_writer_pid(full)
  3. If a PID is found, scan the writer executable through yara.scan_pid(pid)
  4. Attempt sandboxed archive inspection through maybe_inspect_archive(full)
  5. Scan the file directly through yara.scan_file(full)
  6. Queue a delayed secondary scan through delayed.enqueue(full)

This is an updated three-plus-one stage workflow: process scan, archive inspection when applicable, immediate file scan, and delayed file scan.


10. Monitored Directories

10.1 User-Scope Directories

After detecting the active desktop user and resolving the user home directory, the daemon constructs a safe-mode watch list. The current user-scope directories are:

  • $HOME
  • $HOME/.cache
  • $HOME/.local/share
  • $HOME/.local/share/kate/swap
  • $HOME/.config
  • $HOME/.config/Code/Backups
  • $HOME/.PlayOnLinux

10.2 Runtime-Scope Directories

The daemon also attempts to monitor runtime-user directories:

  • /run/user/<uid>
  • /run/user/<uid>/doc

If these paths are inaccessible or protected, they are skipped and logged accordingly.

10.3 System Temporary Directories

The watch list also includes system temporary areas commonly abused during staging or encryption workflows:

  • /tmp
  • /var/tmp

All selected directories are logged explicitly at startup for operational transparency.


11. Main Execution Flow

11.1 Startup Sequence

The daemon startup sequence is currently:

  1. Ensure the log directory and log startup banner
  2. Register signal handlers for SIGINT and SIGTERM
  3. Load or create the shared ransomware token
  4. Detect the active desktop user
  5. Resolve the user home directory via getpwnam()
  6. Validate that the home directory is accessible
  7. Build the monitored directory list WATCH_DIRS
  8. Load YARA rules from:
    /usr/share/BastionGuard/data/yara/

  9. Initialize ArchiveInspector using:
    /usr/libexec/bastionguard/archive_worker

  10. Start the delayed scan thread
  11. Initialize the inotify engine
  12. Start the realtime thread
  13. Enter the main wait loop until running becomes false

11.2 Shutdown Sequence

On termination request, the daemon performs the following shutdown flow:

  1. Log termination request
  2. Join the realtime inotify thread
  3. Stop the delayed queue
  4. Join the delayed queue thread
  5. Invoke clean_shutdown()
  6. Log successful daemon termination

12. Runtime and Security Considerations

  • Authenticated local alerting: every outbound alert to the localhost receiver includes a shared token, reducing the risk of unauthenticated local event injection
  • Layered detection: scanning both the file and the suspected writer process increases the chance of detecting active ransomware tooling
  • Archive-aware detection: suspicious archives are now analyzed through a dedicated sandboxed worker, extending coverage beyond plain file-content YARA matches
  • Delayed verification: delayed rescanning improves resilience against multi-stage writes, atomic replacement, and late content mutation
  • Best-effort PID attribution: writer attribution improves context but is heuristic and intentionally rate-limited to avoid excessive /proc scanning overhead
  • Cross-distro safety: recursive monitoring uses standard filesystem iterators with permission-denied skipping to avoid brittle distro-specific behavior
  • Operational load: broad recursive watch coverage can generate a high event rate; tuning WATCH_DIRS may be required on low-end systems or I/O-heavy desktops
  • YARA rule scope: the current loader accepts only .yara files; .yar support would require an explicit extension to the loader
  • Logging visibility: the daemon emits detailed operational logs useful for forensics and debugging, but production deployments should pair this with a log rotation policy
  • Unused stabilization helper: although reliable_scan() exists to reduce race-related false negatives, the current realtime path calls yara.scan_file() directly, so stabilization is not yet applied in the main hot path